├── .editorconfig ├── .gitattributes ├── .gitignore ├── .prettierrc.json ├── .shellcheckrc ├── Bakefile.sh ├── LICENSE ├── README.md ├── assets └── demo.gif ├── bake ├── basalt.conf ├── basalt.toml ├── demo.tape ├── docs ├── details.md ├── index.md ├── internal │ └── tools.md ├── languages.md ├── plugins │ ├── filters.md │ ├── index.md │ ├── manifest.md │ └── tools.md ├── reference │ └── commands.md ├── roadmap.md └── tutorials │ ├── 1-getting-started.md │ └── 2-plugins.md ├── foxxo.toml ├── pkg ├── bin │ └── woof └── src │ ├── bin │ └── woof.sh │ ├── commands │ ├── woof-exec.sh │ ├── woof-get-version.sh │ ├── woof-init.sh │ ├── woof-install.sh │ ├── woof-list.sh │ ├── woof-plugin-disable.sh │ ├── woof-plugin-enable.sh │ ├── woof-plugin-info.sh │ ├── woof-plugin-install.sh │ ├── woof-plugin-list.sh │ ├── woof-plugin-uninstall.sh │ ├── woof-plugin.sh │ ├── woof-set-version.sh │ ├── woof-tool.sh │ └── woof-uninstall.sh │ ├── filter_utils │ └── util │ │ ├── util.jq │ │ └── util.sh │ └── util │ ├── helper-determine.sh │ ├── helper-plugin.sh │ ├── helper-tool.sh │ ├── helper-toolversions.sh │ ├── helper.sh │ ├── p.sh │ ├── tty.sh │ ├── util-help.sh │ ├── util-plugin.sh │ ├── util-print.sh │ ├── util-tool.sh │ ├── util-toolversions.sh │ ├── util.sh │ └── var.sh └── tests ├── helper_toolversions_parse.bats ├── install.bats ├── stubs └── woof-plugin-basictest │ ├── manifest.ini │ └── tools │ └── tool1.sh ├── toolversions_cd.bats └── util ├── init.sh └── test_utils.sh /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # foxxo start 2 | * text=auto eol=lf 3 | bake linguist-generated 4 | # foxxo end 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .basalt/ 2 | site/ 3 | 4 | # Added by cargo 5 | 6 | /target 7 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "semi": false, 4 | "singleQuote": true, 5 | "trailingComma": "all" 6 | } 7 | -------------------------------------------------------------------------------- /.shellcheckrc: -------------------------------------------------------------------------------- 1 | disable=SC1007 2 | disable=SC2034 3 | -------------------------------------------------------------------------------- /Bakefile.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | task.run() { 4 | ./pkg/bin/woof "$@" 5 | } 6 | 7 | task.test() { 8 | bats ./tests 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Woof 2 | 3 | The version manager to end all version managers 4 | 5 | STATUS: IN DEVELOPMENT 6 | 7 | ## Why? 8 | 9 | - Your OS's package manager doesn't contain the latest (or multiple) `$language` versions 10 | - You want an OS-independent way to install and switch between `$language` versions 11 | - You are tired of installing and configuring version managers (or version manager plugins) for every single language 12 | - More repeatable and reproducible builds 13 | 14 | ## Features 15 | 16 | - _Just Fucking Works_ 17 | - Optionally _configurationless_ 18 | - Clean Bash code 19 | - Favors builtins and native Bash features (over external commands) 20 | - Supports 35 tools on at least `x86_64` (see [roadmap](./docs/roadmap.md) and [languages](./docs/languages.md) for details) 21 | - Compatible with [.tool-versions](https://asdf-vm.com/manage/configuration.html#tool-versions), nvm's [.nvmrc](https://github.com/nvm-sh/nvm#nvmrc) and read things from popular version managers like `rvm` and `pyenv` 22 | - Fast 23 | - Uses no symlinks or shims 24 | 25 | ## Preview (v0.5.0) 26 | 27 | ![Demo](./assets/demo.gif) 28 | 29 | ## Prerequisites 30 | 31 | External utilities are _only_ used due to necessity or efficiency. The following are used 32 | 33 | - cURL 34 | - jq 1.6 35 | - POSIX `mv`, `cat`, `cp`, `uname`, `stty`,, etc. 36 | - sort (TODO: phase out -V GNUism) 37 | - Optional: `pv` 38 | 39 | ## Installation 40 | 41 | Use [Basalt](https://github.com/hyperupcall/basalt), a Bash package manager, to install this project globally 42 | 43 | ```sh 44 | basalt global add hyperupcall/woof 45 | ``` 46 | -------------------------------------------------------------------------------- /assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/version-manager/woof/9c0bfe7ad722a841b501fe48044a839d4d9888c1/assets/demo.gif -------------------------------------------------------------------------------- /bake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # @name Bake 4 | # @brief Bake: A Bash-based Make alternative 5 | # @description Bake is a dead-simple task runner used to quickly cobble together shell scripts 6 | # 7 | # In a few words, Bake lets you call the following 'print' task with './bake print' 8 | # 9 | # ```bash 10 | # #!/usr/bin/env bash 11 | # task.print() { 12 | # printf '%s\n' 'Contrived example' 13 | # } 14 | # ``` 15 | # 16 | # Learn more about it [on GitHub](https://github.com/hyperupcall/bake) 17 | 18 | __global_bake_version='1.11.2' 19 | 20 | if [ "$BAKE_INTERNAL_ONLY_VERSION" = 'yes' ]; then 21 | # This is unused, but keep it here just in case 22 | # shellcheck disable=SC2034 23 | BAKE_INTERNAL_ONLY_VERSION_SUCCESS='yes' 24 | 25 | return 0 26 | fi 27 | 28 | if [ "$0" != "${BASH_SOURCE[0]}" ] && [ "$BAKE_INTERNAL_CAN_SOURCE" != 'yes' ]; then 29 | printf '%s\n' 'Error: This file should not be sourced' >&2 30 | return 1 31 | fi 32 | 33 | # @description Prints `$1` formatted as an error and the stacktrace to standard error, 34 | # then exits with code 1 35 | # @arg $1 string Text to print 36 | bake.die() { 37 | if [ -n "$1" ]; then 38 | __bake_error "$1. Exiting" 39 | else 40 | __bake_error 'Exiting' 41 | fi 42 | __bake_print_big --show-time '<- ERROR' 43 | 44 | __bake_print_stacktrace 45 | 46 | exit 1 47 | } 48 | 49 | # @description Prints `$1` formatted as a warning to standard error 50 | # @arg $1 string Text to print 51 | bake.warn() { 52 | if __bake_is_color; then 53 | printf "\033[1;33m%s:\033[0m %s\n" 'Warn' "$1" 54 | else 55 | printf '%s: %s\n' 'Warn' "$1" 56 | fi 57 | } >&2 58 | 59 | # @description Prints `$1` formatted as information to standard output 60 | # @arg $1 string Text to print 61 | bake.info() { 62 | if __bake_is_color; then 63 | printf "\033[0;34m%s:\033[0m %s\n" 'Info' "$1" 64 | else 65 | printf '%s: %s\n' 'Info' "$1" 66 | fi 67 | } 68 | 69 | # breaking: remove in v2 70 | # @description Dies if any of the supplied variables are empty. Deprecated in favor of 'bake.assert_not_empty' 71 | # @arg $@ string Names of variables to check for emptiness 72 | # @see bake.assert_not_empty 73 | bake.assert_nonempty() { 74 | __bake_internal_warn "Function 'bake.assert_nonempty' is deprecated. Please use 'bake.assert_not_empty' instead" 75 | bake.assert_not_empty "$@" 76 | } 77 | 78 | # @description Dies if any of the supplied variables are empty 79 | # @arg $@ string Names of variables to check for emptiness 80 | bake.assert_not_empty() { 81 | local variable_name= 82 | for variable_name; do 83 | local -n ____variable="$variable_name" 84 | 85 | if [ -z "$____variable" ]; then 86 | bake.die "Failed because variable '$variable_name' is empty" 87 | fi 88 | done; unset -v variable_name 89 | } 90 | 91 | # @description Dies if a command cannot be found 92 | # @arg $1 string Command name to test for existence 93 | bake.assert_cmd() { 94 | local cmd=$1 95 | 96 | if [ -z "$cmd" ]; then 97 | bake.die "Argument must not be empty" 98 | fi 99 | 100 | if ! command -v "$cmd" &>/dev/null; then 101 | bake.die "Failed to find command '$cmd'. Please install it before continuing" 102 | fi 103 | } 104 | 105 | # @description Determine if a flag was passed as an argument 106 | # @arg $1 string Flag name to test for 107 | # @arg $@ string Rest of the arguments to search through 108 | bake.has_flag() { 109 | local flag_name="$1" 110 | 111 | if [ -z "$flag_name" ]; then 112 | bake.die "Argument must not be empty" 113 | fi 114 | if ! shift; then 115 | bake.die 'Failed to shift' 116 | fi 117 | 118 | local -a flags=("$@") 119 | if ((${#flags[@]} == 0)); then 120 | flags=("${__bake_args_userflags[@]}") 121 | fi 122 | 123 | local arg= 124 | for arg in "${flags[@]}"; do 125 | if [ "$arg" = "$flag_name" ]; then 126 | return 0 127 | fi 128 | done; unset -v arg 129 | 130 | return 1 131 | } 132 | 133 | # @description Change the behavior of Bake. See [guide.md](./docs/guide.md) for details 134 | # @arg $1 string Name of config property to change 135 | # @arg $2 string New value of config property 136 | bake.cfg() { 137 | local cfg="$1" 138 | local value="$2" 139 | 140 | # breaking: remove in v2 141 | case $cfg in 142 | stacktrace) 143 | case $value in 144 | yes) __bake_internal_warn "Passing either 'yes' or 'no' as a value for 'bake.cfg stacktrace' is deprecated. Instead, use either 'on' or 'off'"; __bake_cfg_stacktrace='on' ;; 145 | no) __bake_internal_warn "Passing either 'yes' or 'no' as a value for 'bake.cfg stacktrace' is deprecated. Instead, use either 'on' or 'off'"; __bake_cfg_stacktrace='off' ;; 146 | on|off) __bake_cfg_stacktrace=$value ;; 147 | *) __bake_internal_bigdie "Config property '$cfg' accepts only either 'on' or 'off'" ;; 148 | esac 149 | ;; 150 | big-print) 151 | case $value in 152 | yes|no|on|off) __bake_internal_warn "Passing any once-valid value to 'bake.cfg big-print' is deprecated. Instead, use function comments" ;; 153 | *) __bake_internal_bigdie "Config property '$cfg' accepts only either 'on' or 'off'" ;; 154 | esac 155 | ;; 156 | pedantic-task-cd) 157 | case $value in 158 | yes) __bake_internal_warn "Passing either 'yes' or 'no' as a value for 'bake.cfg pedantic-task-cd' is deprecated. Instead, use either 'on' or 'off'"; trap '__bake_trap_debug_fixcd' 'DEBUG' ;; 159 | no) __bake_internal_warn "Passing either 'yes' or 'no' as a value for 'bake.cfg pedantic-task-cd' is deprecated. Instead, use either 'on' or 'off'"; trap - 'DEBUG' ;; 160 | on) trap '__bake_trap_debug_fixcd' 'DEBUG' ;; 161 | off) trap - 'DEBUG' ;; 162 | *) __bake_internal_bigdie "Config property '$cfg' accepts only either 'on' or 'off'" ;; 163 | esac 164 | ;; 165 | *) 166 | __bake_internal_bigdie "No config property matched '$cfg'" 167 | ;; 168 | esac 169 | } 170 | 171 | # @description Prints stacktrace 172 | # @internal 173 | __bake_print_stacktrace() { 174 | if [ "$__bake_cfg_stacktrace" = 'on' ]; then 175 | if __bake_is_color; then 176 | printf '\033[4m%s\033[0m\n' 'Stacktrace:' 177 | else 178 | printf '%s\n' 'Stacktrace:' 179 | fi 180 | 181 | local i= 182 | for ((i=0; i<${#FUNCNAME[@]}-1; ++i)); do 183 | local __bash_source="${BASH_SOURCE[$i]}"; __bash_source=${__bash_source##*/} 184 | printf '%s\n' " in ${FUNCNAME[$i]} ($__bash_source:${BASH_LINENO[$i-1]})" 185 | done; unset -v i __bash_source 186 | fi 187 | } >&2 188 | 189 | # @description Function that is executed when the 'ERR' event is trapped 190 | # @internal 191 | __bake_trap_err() { 192 | local error_code=$? 193 | 194 | __bake_print_big --show-time '<- ERROR' 195 | __bake_internal_error "Your Bakefile did not exit successfully (exit code $error_code)" 196 | __bake_print_stacktrace 197 | 198 | exit $error_code 199 | } >&2 200 | 201 | # @description When running a task, ensure that we start in the correct directory 202 | # @internal 203 | __bake_trap_debug_fixcd_current_fn= 204 | __bake_trap_debug_fixcd() { 205 | local current_function="${FUNCNAME[1]}" 206 | 207 | if [[ $current_function != "$__bake_trap_debug_fixcd_current_fn" \ 208 | && $current_function == task.* ]]; then 209 | if ! cd -- "$BAKE_ROOT"; then 210 | __bake_internal_die "Failed to cd to \$BAKE_ROOT" 211 | fi 212 | fi 213 | 214 | __bake_trap_debug_fixcd_current_fn=$current_function 215 | } >&2 216 | 217 | # @description Ensure that the main function is not ran 218 | # @internal 219 | __bake_trap_debug_barrier() { 220 | local current_function="${FUNCNAME[1]}" 221 | 222 | if [ "$current_function" = '__bake_main' ]; then 223 | # shellcheck disable=SC2034 224 | local __version_old="$__global_bake_version" 225 | 226 | trap - DEBUG 227 | unset -v BAKE_INTERNAL_ONLY_VERSION 228 | unset -v BAKE_INTERNAL_CAN_SOURCE 229 | 230 | __bake_copy_bakescript 231 | if [ "$BAKE_FLAG_UPDATE" = 'yes' ]; then 232 | exit 0 233 | else 234 | # shellcheck disable=SC2154 235 | exec "$BAKE_ROOT/bake" "${__bake_global_backup_args[@]}" 236 | fi 237 | fi 238 | } 239 | 240 | # @description Test whether color should be outputed 241 | # @exitcode 0 if should print color 242 | # @exitcode 1 if should not print color 243 | # @internal 244 | __bake_is_color() { 245 | local fd="1" 246 | 247 | if [ ${NO_COLOR+x} ]; then 248 | return 1 249 | fi 250 | 251 | if [[ $FORCE_COLOR == @(1|2|3) ]]; then 252 | return 0 253 | elif [[ $FORCE_COLOR == '0' ]]; then 254 | return 1 255 | fi 256 | 257 | if [ "$TERM" = 'dumb' ]; then 258 | return 1 259 | fi 260 | 261 | if [ -t "$fd" ]; then 262 | return 0 263 | fi 264 | 265 | return 1 266 | } 267 | 268 | # @description Calls `__bake_internal_error` and terminates with code 1 269 | # @arg $1 string Text to print 270 | # @internal 271 | __bake_internal_die() { 272 | __bake_internal_error "$1. Exiting" 273 | exit 1 274 | } 275 | 276 | # @description Calls `__bake_internal_error` and terminates with code 1. Before 277 | # doing so, it closes with "<- ERROR" big text 278 | # @arg $1 string Text to print 279 | # @internal 280 | __bake_internal_bigdie() { 281 | __bake_print_big '<- ERROR' 282 | 283 | __bake_internal_error "$1. Exiting" 284 | exit 1 285 | } 286 | 287 | # @description Prints `$1` formatted as an internal Bake error to standard error 288 | # @arg $1 Text to print 289 | # @internal 290 | __bake_internal_error() { 291 | if __bake_is_color; then 292 | printf "\033[0;31m%s:\033[0m %s\n" "Error (bake)" "$1" 293 | else 294 | printf '%s: %s\n' 'Error (bake)' "$1" 295 | fi 296 | } >&2 297 | 298 | # @description Prints `$1` formatted as an internal Bake warning to standard error 299 | # @arg $1 Text to print 300 | # @internal 301 | __bake_internal_warn() { 302 | if __bake_is_color; then 303 | printf "\033[0;33m%s:\033[0m %s\n" "Warn (bake)" "$1" 304 | else 305 | printf '%s: %s\n' 'Warn (bake)' "$1" 306 | fi 307 | } >&2 308 | 309 | # @description Prints `$1` formatted as an error to standard error. This is not called because 310 | # I do not wish to surface a public 'bake.error' function. All errors should halt execution 311 | # @arg $1 string Text to print 312 | # @internal 313 | __bake_error() { 314 | if __bake_is_color; then 315 | printf "\033[0;31m%s:\033[0m %s\n" 'Error' "$1" 316 | else 317 | printf '%s: %s\n' 'Error' "$1" 318 | fi 319 | } >&2 320 | 321 | 322 | # @description Tests if the './bake' file should be replaced. It should only 323 | # be replaced if we're not in an interactive Git context 324 | # @internal 325 | __bake_should_replace_bakescript() { 326 | local dir="$BAKE_ROOT" 327 | while [ ! -d "$dir/.git" ] && [[ -n "$dir" ]]; do 328 | dir=${dir%/*} 329 | done 330 | 331 | if [ -d "$dir/.git" ]; then 332 | # ref: https://github.com/git/git/blob/d420dda0576340909c3faff364cfbd1485f70376/wt-status.c#L1749 333 | # ref2: https://github.com/Byron/gitoxide/blob/375051fa97d79f95fa7179b536e616c4aefd88e2/git-repository/src/repository/state.rs#L8 334 | local file= 335 | for file in {rebase-apply/applying,rebase-apply/rebasing,rebase-apply,rebase-merge/interactive,rebase-merge,CHERRY_PICK_HEAD,MERGE_HEAD,BISECT_LOG,REVERT_HEAD}; do 336 | if [ -f "$dir/.git/$file" ]; then 337 | return 1 338 | fi 339 | done; unset -v file 340 | fi 341 | 342 | return 0 343 | } 344 | 345 | # @description Copy 'bake' script to current context 346 | # @internal 347 | __bake_copy_bakescript() { 348 | # If there was an older version, and the versions are different, let the user know 349 | if [ -z ${__version_old+x} ]; then 350 | # shellcheck disable=SC2154 351 | __bake_internal_warn "Updating from version <=1.10.0 to $__version_new" 352 | else 353 | if [ -n "$__version_old" ] && [ "$__version_old" != "$__version_new" ]; then 354 | __bake_internal_warn "Updating from version $__version_old to $__version_new" 355 | fi 356 | fi 357 | 358 | # shellcheck disable=SC2154 359 | if ! cp -f "$__bake_dynamic_script" "$BAKE_ROOT/bake"; then 360 | __bake_internal_die "Failed to copy 'bakeScript.sh'" 361 | fi 362 | if ! printf '\n%s\n' '__bake_main "$@"' >> "$BAKE_ROOT/bake"; then 363 | __bake_internal_die "Failed to append to '$BAKE_ROOT/bake'" 364 | fi 365 | 366 | if ! chmod +x "$BAKE_ROOT/bake"; then 367 | __bake_internal_die "Failed to 'chmod +x' bake script" >&2 368 | fi 369 | } 370 | 371 | # @description Prepares internal variables for time setting 372 | # @internal 373 | __bake_time_prepare() { 374 | if ((BASH_VERSINFO[0] >= 5)); then 375 | __bake_global_timestart=$EPOCHSECONDS 376 | fi 377 | } 378 | 379 | # @description Determines total approximate execution time of a task 380 | # @set string REPLY 381 | # @internal 382 | __bake_time_get_total_pretty() { 383 | unset -v REPLY; REPLY= 384 | 385 | if ((BASH_VERSINFO[0] >= 5)); then 386 | local timediff=$((EPOCHSECONDS - __bake_global_timestart)) 387 | if ((timediff < 1)); then 388 | return 389 | fi 390 | 391 | local seconds=$((timediff % 60)) 392 | local minutes=$((timediff / 60 % 60)) 393 | local hours=$((timediff / 3600 % 60)) 394 | 395 | REPLY="${seconds}s" 396 | 397 | if ((minutes > 0)); then 398 | REPLY="${minutes}m $REPLY" 399 | fi 400 | 401 | if ((hours > 0)); then 402 | REPLY="${hours}h $REPLY" 403 | fi 404 | fi 405 | } 406 | 407 | # @description Parses the configuration for functions embeded in comments. This properly 408 | # parses inherited config from the 'init' function 409 | # @set string __bake_config_docstring 410 | # @set array __bake_config_watchexec_args 411 | # @set object __bake_config_map 412 | # @internal 413 | __bake_parse_task_comments() { 414 | local task_name="$1" 415 | 416 | local tmp_docstring= 417 | local -a tmp_watch_args=() 418 | local -A tmp_cfg_map=() 419 | local line= 420 | while IFS= read -r line || [ -n "$line" ]; do 421 | if [[ $line =~ ^[[:space:]]*#[[:space:]](doc|watch|config):[[:space:]]*(.*?)$ ]]; then 422 | local comment_category="${BASH_REMATCH[1]}" 423 | local comment_content="${BASH_REMATCH[2]}" 424 | 425 | if [ "$comment_category" = 'doc' ]; then 426 | tmp_docstring=$comment_content 427 | elif [ "$comment_category" = 'watch' ]; then 428 | readarray -td' ' tmp_watch_args <<< "$comment_content" 429 | tmp_watch_args[-1]=${tmp_watch_args[-1]::-1} 430 | elif [ "$comment_category" = 'config' ]; then 431 | local -a pairs=() 432 | readarray -td' ' pairs <<< "$comment_content" 433 | pairs[-1]=${pairs[-1]::-1} 434 | 435 | # shellcheck disable=SC1007 436 | local pair= key= value= 437 | for pair in "${pairs[@]}"; do 438 | IFS='=' read -r key value <<< "$pair" 439 | 440 | tmp_cfg_map[$key]=${value:-on} 441 | done; unset -v pair 442 | fi 443 | fi 444 | 445 | # function() 446 | if [[ $line =~ ^([[:space:]]*function[[:space:]]*)?(.*?)[[:space:]]*\(\)[[:space:]]*\{ ]]; then 447 | local function_name="${BASH_REMATCH[2]}" 448 | 449 | if [ "$function_name" == task."$task_name" ]; then 450 | __bake_config_docstring=$tmp_docstring 451 | 452 | __bake_config_watchexec_args+=("${tmp_watch_args[@]}") 453 | 454 | local key= 455 | for key in "${!tmp_cfg_map[@]}"; do 456 | __bake_config_map[$key]=${tmp_cfg_map[$key]} 457 | done; unset -v key 458 | 459 | break 460 | elif [ "$function_name" == 'init' ]; then 461 | __bake_config_watchexec_args+=("${tmp_watch_args[@]}") 462 | 463 | local key= 464 | for key in "${!tmp_cfg_map[@]}"; do 465 | __bake_config_map[$key]=${tmp_cfg_map[$key]} 466 | done; unset -v key 467 | fi 468 | 469 | tmp_docstring= 470 | tmp_watch_args=() 471 | tmp_cfg_map=() 472 | fi 473 | done < "$BAKE_FILE"; unset -v line 474 | } 475 | 476 | # @description Nicely prints all 'Bakefile.sh' tasks to standard output 477 | # @internal 478 | __bake_print_tasks() { 479 | local str=$'Tasks:\n' 480 | 481 | local -a task_flags=() 482 | # shellcheck disable=SC1007 483 | local line= task_docstring= 484 | while IFS= read -r line || [ -n "$line" ]; do 485 | # doc 486 | if [[ $line =~ ^[[:space:]]*#[[:space:]]doc:[[:space:]](.*?) ]]; then 487 | task_docstring=${BASH_REMATCH[1]} 488 | fi 489 | 490 | # flag 491 | if [[ $line =~ bake\.has_flag[[:space:]][\'\"]?([[:alnum:]]+) ]]; then 492 | task_flags+=("[--${BASH_REMATCH[1]}]") 493 | fi 494 | 495 | if [[ $line =~ ^([[:space:]]*function[[:space:]]*)?task\.(.*?)\(\)[[:space:]]*\{[[:space:]]*(#[[:space:]]*(.*))? ]]; then 496 | local matched_function_name="${BASH_REMATCH[2]}" 497 | local matched_comment="${BASH_REMATCH[4]}" 498 | 499 | if ((${#task_flags[@]} > 0)); then 500 | str+=" ${task_flags[*]}"$'\n' 501 | fi 502 | task_flags=() 503 | 504 | str+=" -> $matched_function_name" 505 | 506 | if [[ -n "$matched_comment" || -n "$task_docstring" ]]; then 507 | if [ -n "$matched_comment" ]; then 508 | __bake_internal_warn "Adjacent documentation comments are deprecated. Instead, write a comment above 'task.$matched_function_name()' like so: '# doc: $matched_comment'" 509 | task_docstring=$matched_comment 510 | fi 511 | 512 | if __bake_is_color; then 513 | str+=$' \033[3m'"($task_docstring)"$'\033[0m' 514 | else 515 | str+=" ($task_docstring)" 516 | fi 517 | fi 518 | 519 | str+=$'\n' 520 | task_docstring= 521 | fi 522 | done < "$BAKE_FILE"; unset -v line 523 | 524 | if [ -z "$str" ]; then 525 | if __bake_is_color; then 526 | str=$' \033[3mNo tasks\033[0m\n' 527 | else 528 | str=$' No tasks\n' 529 | fi 530 | fi 531 | 532 | printf '%s' "$str" 533 | } >&2 534 | 535 | # @description Prints text that takes up the whole terminal width 536 | # @arg $1 string Text to print 537 | # @internal 538 | __bake_print_big() { 539 | if [ "${__bake_config_map[big-print]}" = 'off' ]; then 540 | return 541 | fi 542 | 543 | if [ "$1" = '--show-time' ]; then 544 | local flag_show_time='yes' 545 | local print_text="$2" 546 | else 547 | local flag_show_time='no' 548 | local print_text="$1" 549 | fi 550 | 551 | __bake_time_get_total_pretty 552 | local time_str="${REPLY:+ ($REPLY) }" 553 | 554 | # shellcheck disable=SC1007 555 | local _stty_height= _stty_width= 556 | read -r _stty_height _stty_width < <( 557 | if stty size &>/dev/null; then 558 | stty size 559 | else 560 | # Only columns is used by Bake, so '20 was chosen arbitrarily 561 | if [ -n "$COLUMNS" ]; then 562 | printf '%s\n' "20 $COLUMNS" 563 | else 564 | printf '%s\n' '20 80' 565 | fi 566 | fi 567 | ) 568 | 569 | local separator_text= 570 | # shellcheck disable=SC2183 571 | printf -v separator_text '%*s' $((_stty_width - ${#print_text} - 1)) 572 | printf -v separator_text '%s' "${separator_text// /=}" 573 | if [[ "$flag_show_time" == 'yes' && -n "$time_str" ]]; then 574 | separator_text="${separator_text::5}${time_str}${separator_text:5+${#time_str}:${#separator_text}}" 575 | fi 576 | if __bake_is_color; then 577 | printf '\033[1m%s %s\033[0m\n' "$print_text" "$separator_text" 578 | else 579 | printf '%s %s\n' "$print_text" "$separator_text" 580 | fi 581 | } >&2 582 | 583 | # @description Parses the arguments. This also includes setting the the 'BAKE_ROOT' 584 | # and 'BAKE_FILE' variables 585 | # @set REPLY Number of times to shift 586 | # @internal 587 | __bake_parse_args() { 588 | unset -v REPLY; REPLY= 589 | local -i total_shifts=0 590 | 591 | local arg= 592 | for arg; do case $arg in 593 | -f) 594 | BAKE_FILE=$2 595 | if [ -z "$BAKE_FILE" ]; then 596 | __bake_internal_die "A value was not specified for for flag '-f'" 597 | fi 598 | ((total_shifts += 2)) 599 | if ! shift 2; then 600 | __bake_internal_die 'Failed to shift' 601 | fi 602 | 603 | if [ ! -e "$BAKE_FILE" ]; then 604 | __bake_internal_die "Specified file '$BAKE_FILE' does not exist" 605 | fi 606 | if [ ! -f "$BAKE_FILE" ]; then 607 | __bake_internal_die "Specified file '$BAKE_FILE' is not actually a file" 608 | fi 609 | ;; 610 | -h) 611 | local flag_help='yes' 612 | if ! shift; then 613 | __bake_internal_die 'Failed to shift' 614 | fi 615 | ;; 616 | -w) 617 | ((total_shifts += 1)) 618 | if ! shift; then 619 | __bake_internal_die 'Failed to shift' 620 | fi 621 | 622 | if [[ ! -v 'BAKE_INTERNAL_NO_WATCH_OVERRIDE' ]]; then 623 | BAKE_FLAG_WATCH='yes' 624 | fi 625 | ;; 626 | -u) 627 | ((total_shifts += 1)) 628 | if ! shift; then 629 | __bake_internal_die 'Failed to shift' 630 | fi 631 | 632 | BAKE_FLAG_UPDATE='yes' 633 | ;; 634 | -v) 635 | printf '%s\n' "Version: $__global_bake_version" 636 | exit 0 637 | ;; 638 | *) 639 | break 640 | ;; 641 | esac done; unset -v arg 642 | 643 | if [ -n "$BAKE_FILE" ]; then 644 | BAKE_ROOT=$( 645 | # shellcheck disable=SC1007 646 | CDPATH= cd -- "${BAKE_FILE%/*}" 647 | printf '%s\n' "$PWD" 648 | ) 649 | BAKE_FILE="$BAKE_ROOT/${BAKE_FILE##*/}" 650 | else 651 | if ! BAKE_ROOT=$( 652 | while [ ! -f './Bakefile.sh' ] && [ "$PWD" != / ]; do 653 | if ! cd ..; then 654 | exit 1 655 | fi 656 | done 657 | 658 | if [ "$PWD" = / ]; then 659 | exit 1 660 | fi 661 | 662 | printf '%s' "$PWD" 663 | ); then 664 | __bake_internal_die "Failed to find 'Bakefile.sh'" 665 | fi 666 | BAKE_FILE="$BAKE_ROOT/Bakefile.sh" 667 | fi 668 | 669 | if [ "$flag_help" = 'yes' ]; then 670 | cat <<-"EOF" 671 | Usage: bake [-h|-v] [-u|-w] [-f ] [var=value ...] [args ...] 672 | EOF 673 | __bake_print_tasks 674 | exit 675 | fi 676 | 677 | REPLY=$total_shifts 678 | } 679 | 680 | # @description Main function 681 | # @internal 682 | __bake_main() { 683 | # Environment and configuration boilerplate 684 | set -ETeo pipefail 685 | shopt -s dotglob extglob globasciiranges globstar lastpipe shift_verbose 686 | export LANG='C' LC_CTYPE='C' LC_NUMERIC='C' LC_TIME='C' LC_COLLATE='C' \ 687 | LC_MONETARY='C' LC_MESSAGES='C' LC_PAPER='C' LC_NAME='C' LC_ADDRESS='C' \ 688 | LC_TELEPHONE='C' LC_MEASUREMENT='C' LC_IDENTIFICATION='C' LC_ALL='C' 689 | trap '__bake_trap_err' 'ERR' 690 | trap ':' 'INT' # Ensure Ctrl-C ends up printing <- ERROR ==== etc. 691 | 692 | declare -ga __bake_args_original=("$@") 693 | 694 | # Parse arguments 695 | # Set `BAKE_{ROOT,FILE,FLAG_WATCH}` 696 | BAKE_ROOT=; BAKE_FILE=; BAKE_FLAG_WATCH= 697 | __bake_parse_args "$@" 698 | if ! shift $REPLY; then 699 | __bake_internal_die 'Failed to shift' 700 | fi 701 | 702 | # Set variables à la Make 703 | # shellcheck disable=SC1007 704 | local __bake_key= __bake_value= __bake_arg= 705 | for __bake_arg; do case $__bake_arg in 706 | *=*) 707 | IFS='=' read -r __bake_key __bake_value <<< "$__bake_arg" 708 | 709 | # If 'key=value' is passed, create global variable $value 710 | declare -g "$__bake_key" 711 | local -n __bake_variable="$__bake_key" 712 | __bake_variable="$__bake_value" 713 | 714 | # If 'key=value' is passed, create global variable $value_key 715 | declare -g "var_$__bake_key" 716 | local -n __bake_variable="var_$__bake_key" 717 | __bake_variable="$__bake_value" 718 | 719 | if ! shift; then 720 | __bake_internal_die 'Failed to shift' 721 | fi 722 | ;; 723 | *) 724 | break 725 | ;; 726 | esac done; unset -v __bake_arg 727 | unset -v __bake_key __bake_value 728 | unset -vn __bake_variable 729 | 730 | local __bake_task="$1" 731 | if [ -z "$__bake_task" ]; then 732 | __bake_internal_error 'No valid task supplied' 733 | __bake_print_tasks 734 | exit 1 735 | fi 736 | if ! shift; then 737 | __bake_internal_die 'Failed to shift' 738 | fi 739 | 740 | declare -ga __bake_args_userflags=("$@") 741 | 742 | declare -g __bake_config_docstring= 743 | declare -ga __bake_config_watchexec_args=() 744 | declare -gA __bake_config_map=( 745 | [stacktrace]='off' 746 | [big-print]='on' 747 | [pedantic-cd]='off' 748 | ) 749 | 750 | if [ "$BAKE_FLAG_WATCH" = 'yes' ]; then 751 | if ! command -v watchexec &>/dev/null; then 752 | __bake_internal_die "Executable not found: 'watchexec'" 753 | fi 754 | 755 | __bake_parse_task_comments "$__bake_task" 756 | 757 | # shellcheck disable=SC1007 758 | BAKE_INTERNAL_NO_WATCH_OVERRIDE= exec watchexec "${__bake_config_watchexec_args[@]}" "$BAKE_ROOT/bake" -- "${__bake_args_original[@]}" 759 | else 760 | if ! cd -- "$BAKE_ROOT"; then 761 | __bake_internal_die "Failed to cd" 762 | fi 763 | 764 | # shellcheck disable=SC2097,SC1007,SC1090 765 | __bake_task= source "$BAKE_FILE" 766 | 767 | if declare -f task."$__bake_task" >/dev/null 2>&1; then 768 | __bake_parse_task_comments "$__bake_task" 769 | 770 | __bake_print_big "-> RUNNING TASK '$__bake_task'" 771 | 772 | if declare -f init >/dev/null 2>&1; then 773 | init "$__bake_task" 774 | fi 775 | 776 | __bake_time_prepare 777 | 778 | task."$__bake_task" "${__bake_args_userflags[@]}" 779 | 780 | __bake_print_big --show-time "<- DONE" 781 | else 782 | __bake_internal_error "Task '$__bake_task' not found" 783 | __bake_print_tasks 784 | exit 1 785 | fi 786 | fi 787 | } 788 | 789 | __bake_entrypoint() { 790 | printf '%s\n' 'Not implemented.' 791 | } 792 | 793 | if [[ -v BAKE_INTERNAL_EXPERIMENTAL_SINGLEFILE ]]; then 794 | __bake_entrypoint "$@" 795 | fi 796 | 797 | __bake_main "$@" 798 | -------------------------------------------------------------------------------- /basalt.conf: -------------------------------------------------------------------------------- 1 | [package] 2 | type = bash 3 | name = woof 4 | namespace = woof 5 | version = 0.5.0 6 | author = Edwin Kofler 7 | description = The version manager to end all version managers 8 | 9 | [run] 10 | dependency = https://github.com/hyperupcall/bats-all@v4.6.0 11 | dependency = https://github.com/hyperupcall/bash-core@v0.12.0 12 | dependency = https://github.com/hyperupcall/bash-term@v0.6.3 13 | dependency = https://github.com/hyperupcall/bash-utility@v0.4.0 14 | sourceDir = pkg/src/util 15 | binDir = pkg/bin 16 | -------------------------------------------------------------------------------- /basalt.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | type = 'app' 3 | name = 'woof' 4 | slug = 'woof' 5 | version = '0.5.0' 6 | authors = ['Edwin Kofler '] 7 | description = 'The version manager to end all version managers' 8 | 9 | [run] 10 | dependencies = [ 11 | 'https://github.com/hyperupcall/bats-all@v4.6.0', 12 | 'https://github.com/hyperupcall/bash-core@v0.12.0', 13 | 'https://github.com/hyperupcall/bash-term@v0.6.3', 14 | 'https://github.com/hyperupcall/bash-utility@v0.4.0', 15 | ] 16 | sourceDirs = ['pkg/src/util'] 17 | builtinDirs = [] 18 | binDirs = ['pkg/bin'] 19 | completionDirs = [] 20 | manDirs = [] 21 | 22 | [run.shellEnvironment] 23 | 24 | [run.setOptions] 25 | errexit = 'on' 26 | pipefail = 'on' 27 | 28 | [run.shoptOptions] 29 | shift_verbose = 'on' 30 | -------------------------------------------------------------------------------- /demo.tape: -------------------------------------------------------------------------------- 1 | Sleep 1s 2 | Type "woof" 3 | Enter 4 | Sleep 1.5s 5 | Type "woof install" 6 | Enter 7 | Sleep 500ms 8 | Type "j" 9 | Sleep 500ms 10 | Enter 11 | Sleep 500ms 12 | Type "jjkjk" 13 | Enter 14 | Sleep 500ms 15 | Type "jjk" 16 | Sleep 500ms 17 | Enter 18 | Sleep 6.5s 19 | Ctrl+D 20 | -------------------------------------------------------------------------------- /docs/details.md: -------------------------------------------------------------------------------- 1 | # Details 2 | 3 | Woof supports global and local versions. 4 | 5 | ## Approach 6 | 7 | Woof neither uses shims or symlinks. Symlinks aren't used because they typically must be recreated, sometimes frequently (ex. `npm -g i http-server`). Shims aren't good since they must be recreated (reshimed) as well, but they have an additional performance overhead. 8 | 9 | Instead, Woof manually manages the `PATH`. When initializing, `PATH` is set to the global defaults of each tool. When `cd`'ing, `PATH` is automatically changed, depending on any discovered `.tool-version` files. 10 | 11 | ## Plugin Installation 12 | 13 | Unlike other version managers, Woof can handle different version of many languages without installing any extra plugins. 14 | 15 | Woof allows to enable and disable plugins if they are causing trouble. 16 | 17 | ## Language Specifics 18 | 19 | Differences from stock configuration. These explain how `` 8 | - Add the `woof-plugin` topic or tag to the repository 9 | 10 | At a minimum, a plugin must include a manifest. See [Manifest](./manifest.md) for details. 11 | 12 | Plugins also contain a `tools` and possibly a `filters` directory. See [Tools](./tools.md) and [Filters](./filters.md) for details. 13 | -------------------------------------------------------------------------------- /docs/plugins/manifest.md: -------------------------------------------------------------------------------- 1 | # Manifest 2 | 3 | Use the `manifest.ini` file to store metadata about your plugin. 4 | 5 | ## `manifest.ini` 6 | 7 | ### `slug` 8 | 9 | The value of this _must_ match that of your repository name. For example, if your repository is called `woof-plugin-hashicorp`, this key must have a value of `hashicorp` 10 | 11 | ### `name` 12 | 13 | ### `description` 14 | 15 | ### `tags` 16 | -------------------------------------------------------------------------------- /docs/plugins/tools.md: -------------------------------------------------------------------------------- 1 | # Plugin API 2 | 3 | ## `.env()` 4 | 5 | Called when a plugin needs to set the environment 6 | 7 | This may also be called to hook into `cd`. For example, the NodeJS plugin needs to change versions depending on not only `.tool-versions`, but also `.nvm` and `.node-version` 8 | 9 | By default, this automatically does the correct parsing of `.tool-versions` for the particular plugin 10 | 11 | ## `.table()` 12 | 13 | Prints a version table to standard output. Each line of standard output looks like the following: 14 | 15 | ```txt 16 | ||||[|] 17 | ``` 18 | 19 | Here are two examples: 20 | 21 | ```txt 22 | Go|v1.17.6|linux|x86_64|https://go.dev/dl/dl/go1.17.6.linux-amd64.tar.gz 23 | NodeJS|v15.9.0|linux|x86_64|https://nodejs.org/download/release/v15.9.0/node-v15.9.0-linux-x64.tar.gz|2021-02-18 24 | ``` 25 | 26 | To see the exact supported values for `` and ``, see [roadmap.md](../roadmap.md) 27 | 28 | ## `.install()` 29 | 30 | Downloads and extracts a particular version of a language. The following positional parameters are set: 31 | 32 | - `$1`: url 33 | - `$2`: version (does not contain `v` prefix) 34 | - `$3`: os 35 | - `$4`: arch 36 | 37 | Set the following variables for installation to complete successfully. `REPLY_DIR` is the only requried variable 38 | 39 | - `REPLY_DIR=` 40 | - `REPLY_BINS=()` 41 | - `REPLY_INCLUDES=()` 42 | - `REPLY_LIBS=()` 43 | - `REPLY_MANS=()` 44 | - `REPLY_BASH_COMPLETIONS=()` 45 | - `REPLY_ZSH_COMPLETIONS=()` 46 | - `REPLY_FISH_COMPLETIONS=()` 47 | 48 | Persisted state across installs / uninstalls 49 | -------------------------------------------------------------------------------- /docs/reference/commands.md: -------------------------------------------------------------------------------- 1 | # commands 2 | 3 | ## `plugin-install ` 4 | 5 | Depending on your input, Woof will intelligently determine where to download the plugin from. 6 | 7 | 8 | | Initial String | Final URL | 9 | | --------------------------------------------------- | --------------------------------------------------- | 10 | | blah | https://github.com/version-manager/woof-plugin-blah | 11 | | woof-plugin-blah | https://github.com/version-manager/woof-plugin-blah | 12 | | version-manager/woof-plugin-blah | https://github.com/version-manager/woof-plugin-blah | 13 | | github.com/version-manager/woof-plugin-blah | https://github.com/version-manager/woof-plugin-blah | 14 | | https://github.com/version-manager/woof-plugin-blah | https://github.com/version-manager/woof-plugin-blah | 15 | 16 | 17 | If you specify nothing, then you are able to select from a list of first-party plugins. 18 | 19 | It is also possible to specify an _absolute_ or a _relative_ path to a plugin directory. 20 | -------------------------------------------------------------------------------- /docs/roadmap.md: -------------------------------------------------------------------------------- 1 | # Roadmap 2 | 3 | There are three dimensions of support for each runtime 4 | 5 | - Shell 6 | - Completions 7 | - Shell init 8 | - Operating System 9 | - Architecture 10 | 11 | ## Shell 12 | 13 | Supported shells will include 14 | 15 | - Bash 16 | - Zsh 17 | - Ksh 18 | - Fish 19 | - Elvish 20 | - Oil 21 | 22 | Right now, Bash is best supported 23 | 24 | ## Operating System 25 | 26 | Supported operating systems will include 27 | 28 | - `linux` 29 | - `freebsd` 30 | - `darwin` 31 | - `windows` 32 | 33 | - `aix` 34 | - `openbsd` 35 | - `netbsd` 36 | - `solaris` (`sunOS`) 37 | 38 | Right now, Linux is best supported 39 | 40 | ## Architecture 41 | 42 | Supported architectures will include 43 | 44 | - `x86_64` 45 | - `x86` 46 | - `arm64` 47 | - `armv7l` 48 | - `armv6` 49 | 50 | - `ppc64` (nodejs) 51 | - `ppc64le` (go, nodejs) 52 | - `s390x` (go, nodejs) 53 | - `riscv64` (zig) 54 | 55 | Right now, x86_64 is best supported 56 | -------------------------------------------------------------------------------- /docs/tutorials/1-getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | You may have experienced the trouble of attempting to use two different versions of the same tool. Whether you wanted to switch between Python 3.9 and Python 3.10 or Node.js 18 and Node.js 20, it is a pain to manage them manually. 4 | 5 | Woof solves this. Let's take Node.js as an example. Let's say you wish to install Node.js 18 and 20. You would run the following commands: 6 | 7 | ```console 8 | $ woof install nodejs 9 | v20.8.1 10 | v20.8.0 11 | v20.7.0 12 | v20.6.1 13 | v20.6.0 14 | v20.5.1 15 | v20.5.0 16 | ``` 17 | 18 | As you can see, it will show an interactive list of Node.js versions that you can install. Select the one you want. 19 | 20 | ```console 21 | $ woof install nodejs v20.8.1 22 | Info: Gathering versions 23 | Info: Fetching https://nodejs.org/download/release/v20.8.1/node-v20.8.1-linux-x64.tar.gz 24 | ############################################################################################################################################################################# 100.0% 25 | Info: Unpacking 26 | Info: Installed v20.8.1 27 | Info: Set version 'v20.8.1' as global version 28 | ``` 29 | 30 | If this is the first version of a tool that you install, then it will automatically be selected as the "default version". 31 | 32 | ```console 33 | $ node --version 34 | v20.8.1 35 | ``` 36 | 37 | If you already have a default selected, you will need to switch it yourself. 38 | 39 | Now, let's install a version 18: 40 | 41 | ```console 42 | $ woof install nodejs v18.18.2 43 | Info: Gathering versions 44 | Info: Fetching https://nodejs.org/download/release/v18.18.2/node-v18.18.2-linux-x64.tar.gz 45 | ############################################################################################################################################################################# 100.0% 46 | Info: Unpacking 47 | Info: Installed v18.18.2 48 | ``` 49 | 50 | Notice that it only installed the verison, it did not automatically swithc to this version. For that, you will need to use the `set-version` command: 51 | 52 | ```console 53 | $ woof set-version nodejs v18.18.2 54 | Info: Set version 'v18.18.2' as global version 55 | $ node --version 56 | v18.18.2 57 | ``` 58 | -------------------------------------------------------------------------------- /docs/tutorials/2-plugins.md: -------------------------------------------------------------------------------- 1 | # Plugins 2 | 3 | Plugins enable Woof to support a wide variety of languages and runtimes. 4 | 5 | The official plugins can be found at [github.com/version-manager](https://github.com/version-manager). 6 | 7 | You can find community plugins with the [woof-plugin](https://github.com/topics/woof-plugin) tag on GitHub. 8 | 9 | Adding a plugin is simple. First, let's see what plugins you already have. Use the `woof plugin` subcommand for this: 10 | 11 | ```console 12 | $ woof plugin list 13 | ancillary: 14 | name: Ancillary Tools 15 | desc: Semi-popular tools 16 | tags: N/A 17 | type: git-repository 18 | enabled: yes 19 | core: 20 | name: Core 21 | desc: Popular tools 22 | tags: N/A 23 | type: git-repository 24 | enabled: yes 25 | ``` 26 | 27 | Note that the output will likely be different in future versions. Let's say we want to install the [woof-plugin-hashicorp](https://github.com/version-manager/woof-plugin-hashicorp). Use the `plugin install` command: 28 | 29 | ```console 30 | $ woof plugin install woof plugin install https://github.com/version-manager/woof-plugin-hashicorp 31 | Info: Cloned: /home/edwin/.local/state/woof/plugins/woof-plugin-hashicorp 32 | ``` 33 | 34 | Now, you can install any of the tools that this plugin supports. Let's install [Terraform](https://www.terraform.io): 35 | 36 | ```console 37 | $ woof install hashicorp/terraform 38 | Info: Gathering versions 39 | Warn: Version 'v1.5.3' is already installed for plugin 'hashicorp/terraform'. Switching to that version 40 | Info: Set version 'v1.5.3' as global version 41 | ``` 42 | 43 | As you can see, I chose `v1.5.3` from the GUI. I already have `v1.5.3` version of Terraform installed. Woof automatically switched to that version. 44 | -------------------------------------------------------------------------------- /foxxo.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | ecosystem = "basalt" 3 | form = "app" 4 | for = "anyone" 5 | status = "experimental" 6 | 7 | [discovery] 8 | categories = [] 9 | tags = [] 10 | -------------------------------------------------------------------------------- /pkg/bin/woof: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # eval "$(basalt-package-init --no-assert-version woof)" 4 | # __run "$@" 5 | 6 | eval "$(basalt-package-init)" 7 | basalt.package-init || exit 8 | basalt.package-load 9 | 10 | source "$BASALT_PACKAGE_DIR/pkg/src/bin/woof.sh" 11 | main.woof "$@" 12 | -------------------------------------------------------------------------------- /pkg/src/bin/woof.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | main.woof() { 4 | # If current Bash doesn't meet minimum requirements, search for one that does. Woof is able 5 | # to install ancient versions of Bash, so this logic significantly improves UX 6 | if ! ((BASH_VERSINFO[0] >= 5 || (BASH_VERSINFO[0] >= 4 && BASH_VERSINFO[1] >= 3) )); then 7 | if [ -n "${BASH_INTERNAL_VERSIONEXEC+x}" ]; then 8 | printf '%s\n' "Error: Woof: Exec'ed, but version requirements still not satisfied. Aborting to prevent infinite loops" 9 | exit 1 10 | fi 11 | 12 | local -a bin_dirs=() 13 | IFS=':' read -ra bin_dirs <<< "$PATH" 14 | 15 | local bin_dir= 16 | for bin_dir in "${bin_dirs[@]}"; do 17 | if [ -x "$bin_dir/bash" ]; then 18 | local output= 19 | output=$("$bin_dir/bash" --version) 20 | output=${output#"GNU bash, version "} 21 | 22 | local major_ver=${output%%.*} 23 | output=${output#*.} 24 | local minor_ver=${output%%.*} 25 | 26 | if ((major_ver >= 5 || (major_ver >= 4 && minor_ver >= 3) )); then 27 | printf '%s\n' "Warning: Woof: Bash version from '/usr/bin/env bash' of '$BASH_VERSION' is too low; exec'ing into a newer one: $bin_dir/bash" >&2 28 | BASH_INTERNAL_VERSIONEXEC= exec "$bin_dir/bash" "${BASH_SOURCE[1]}" "$@" 29 | fi 30 | fi 31 | done; unset -v bin_dir 32 | 33 | printf '%s\n' "Error: Woof: Failed to meet minimum Bash requirement of 4.3 and failed to find newer version in PATH" >&2 34 | exit 1 35 | fi 36 | 37 | global_trap_err() { 38 | core.print_stacktrace 39 | } 40 | core.trap_add 'global_trap_err' 'ERR' 41 | 42 | global_stty_saved= 43 | g_tty_height= 44 | g_tty_width= 45 | 46 | : "${WOOF_CONFIG_HOME:=${XDG_CONFIG_HOME:-$HOME/.config}/woof}" 47 | : "${WOOF_CACHE_HOME:=${XDG_CACHE_HOME:-$HOME/.cache}/woof}" 48 | : "${WOOF_DATA_HOME:=${XDG_DATA_HOME:-$HOME/.local/share}/woof}" 49 | : "${WOOF_STATE_HOME:=${XDG_STATE_HOME:-$HOME/.local/state}/woof}" 50 | WOOF_VARS='WOOF_CONFIG_HOME WOOF_CACHE_HOME WOOF_DATA_HOME WOOF_STATE_HOME' 51 | 52 | # Validate the existence of GitHub token 53 | if [ "$WOOF_INTERNAL_TESTING" != 'yes' ]; then 54 | local token_file="$WOOF_DATA_HOME/token" 55 | if [ -f "$token_file" ]; then 56 | if ! GITHUB_TOKEN=$(<"$token_file"); then 57 | util.print_error_die "Failed to read from file '$token_file'" 58 | fi 59 | export GITHUB_TOKEN 60 | else 61 | util.print_error_die "Must have a file containing your GitHub token at '$token_file'" 62 | fi 63 | unset -v token_file 64 | fi 65 | 66 | # Parse arguments 67 | local g_flag_quiet='no' 68 | local arg= 69 | for arg; do case $arg in 70 | --help|-h) 71 | util.help_show_cmd_root_all 72 | exit 0 73 | ;; 74 | --quiet|-q) 75 | # shellcheck disable=SC2034 76 | g_flag_quiet='yes' 77 | if ! shift; then 78 | util.print_fatal_die 'Failed to shift' 79 | fi 80 | ;; 81 | -*) 82 | util.print_error_die "Global flag '$arg' not recognized" 83 | ;; 84 | *) 85 | break 86 | ;; 87 | esac done; unset -v arg 88 | 89 | # Get action name 90 | local subcommand="$1" 91 | if [ -z "$subcommand" ]; then 92 | util.help_show_cmd_root_all 93 | util.print_error_die 'No subcommand was given' 94 | fi 95 | if ! shift; then 96 | util.print_fatal_die 'Failed to shift' 97 | fi 98 | 99 | # shellcheck disable=SC1090 100 | case $subcommand in 101 | init) 102 | source "$BASALT_PACKAGE_DIR/pkg/src/commands/woof-$subcommand.sh" 103 | woof-init "$@" 104 | ;; 105 | install) 106 | source "$BASALT_PACKAGE_DIR/pkg/src/commands/woof-$subcommand.sh" 107 | woof-install "$@" 108 | ;; 109 | uninstall) 110 | source "$BASALT_PACKAGE_DIR/pkg/src/commands/woof-$subcommand.sh" 111 | woof-uninstall "$@" 112 | ;; 113 | get-version) 114 | source "$BASALT_PACKAGE_DIR/pkg/src/commands/woof-$subcommand.sh" 115 | woof-get-version "$@" 116 | ;; 117 | set-version) 118 | source "$BASALT_PACKAGE_DIR/pkg/src/commands/woof-$subcommand.sh" 119 | woof-set-version "$@" 120 | ;; 121 | exec) 122 | source "$BASALT_PACKAGE_DIR/pkg/src/commands/woof-$subcommand.sh" 123 | woof-exec "$@" 124 | ;; 125 | list) 126 | source "$BASALT_PACKAGE_DIR/pkg/src/commands/woof-$subcommand.sh" 127 | woof-list "$@" 128 | ;; 129 | plugin) 130 | source "$BASALT_PACKAGE_DIR/pkg/src/commands/woof-$subcommand.sh" 131 | woof-plugin "$@" 132 | ;; 133 | tool) 134 | source "$BASALT_PACKAGE_DIR/pkg/src/commands/woof-$subcommand.sh" 135 | woof-tool "$@" 136 | ;; 137 | *) 138 | util.print_error_die "Subcommand '$subcommand' not recognized" 139 | ;; 140 | esac 141 | } 142 | -------------------------------------------------------------------------------- /pkg/src/commands/woof-exec.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | woof-exec() { 4 | local -a args=() 5 | local arg= 6 | for arg; do case $arg in 7 | --help) 8 | util.help_show_usage_and_flags 'exec' 9 | util.help_show_part '.exec' 10 | exit 0 11 | ;; 12 | -*) 13 | util.print_help_die '.exec' "Flag '$arg' not recognized" 14 | ;; 15 | *) 16 | args+=("$arg") 17 | esac done; unset -v arg 18 | 19 | local tool_name="${args[0]}" 20 | if [ -z "$tool_name" ]; then 21 | util.print_help_die '.exec' "Passed tool cannot be empty" 22 | fi 23 | 24 | local tool_version="${args[1]}" 25 | if [ -z "$tool_version" ]; then 26 | util.print_help_die '.exec' "Passed version cannot be empty" 27 | fi 28 | 29 | local executable="${args[2]}" 30 | if [ -z "$executable" ]; then 31 | util.print_help_die '.exec' "Passed executable cannot be empty" 32 | fi 33 | 34 | var.get_dir 'tools' "$tool_name" 35 | local install_dir="$REPLY" 36 | 37 | util.get_plugin_data "$tool_name" "$tool_version" 'bins' 38 | local -a bin_dirs="${REPLY[@]}" 39 | 40 | for bin_dir in "${bin_dirs[@]}"; do 41 | for bin_file in "$install_dir/$tool_version/$bin_dir"/*; do 42 | local bin_name="${bin_file##*/}" 43 | if [[ -x "$bin_file" && "$bin_name" == "$executable" ]]; then 44 | exec -a "$executable" "$bin_file" "${args[@]:3}" 45 | fi 46 | done 47 | done; unset -v bin_dir 48 | } 49 | -------------------------------------------------------------------------------- /pkg/src/commands/woof-get-version.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | woof-get-version() { 4 | local -a args=() 5 | local flag_global='no' 6 | local arg= 7 | for arg; do case $arg in 8 | --help) 9 | util.help_show_usage_and_flags 'get-version' 10 | util.help_show_part '.get-version' 11 | exit 0 12 | ;; 13 | --global) 14 | flag_global='yes' 15 | ;; 16 | -*) 17 | util.print_help_die '.get-version' "Flag '$arg' not recognized" 18 | ;; 19 | *) 20 | args+=("$arg") 21 | esac done; unset -v arg 22 | 23 | helper.determine_tool_pair_active "${args[0]}" 24 | declare -g g_tool_pair="$REPLY1" 25 | declare -g g_plugin_name="$REPLY2" 26 | declare -g g_tool_name="$REPLY3" 27 | 28 | local tool_version= 29 | if [ "$flag_global" = 'yes' ]; then 30 | util.tool_get_global_version --no-error "$g_tool_pair" 31 | tool_version="$REPLY" 32 | 33 | if [ -z "$tool_version" ]; then 34 | core.print_warn "No global default was found for plugin '$g_tool_pair'" 35 | return 36 | fi 37 | else 38 | util.tool_get_local_version --no-error "$g_tool_pair" 39 | tool_version="$REPLY" 40 | 41 | if [ -z "$tool_version" ]; then 42 | core.print_warn "No local default was found for plugin '$g_tool_pair'" 43 | return 44 | fi 45 | fi 46 | 47 | printf '%s\n' "$tool_version" 48 | } 49 | -------------------------------------------------------------------------------- /pkg/src/commands/woof-init.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | woof-init() { 4 | local -a args=() 5 | local flag_no_cd='no' 6 | local arg= 7 | for arg; do case $arg in 8 | --help) 9 | util.help_show_usage_and_flags 'init' 10 | util.help_show_part '.init' 11 | exit 0 12 | ;; 13 | --no-cd) 14 | flag_no_cd='yes' 15 | ;; 16 | -*) 17 | util.print_help_die '.init' "Flag '$arg' not recognized" 18 | ;; 19 | *) 20 | args+=("$arg") 21 | esac done; unset -v arg 22 | 23 | local shell="${args[0]}" 24 | 25 | if [ -z "$shell" ]; then 26 | util.print_error_die 'Shell not specified' 27 | fi 28 | 29 | if [[ $shell != @(fish|zsh|ksh|bash|sh) ]]; then 30 | util.print_error_die 'Shell not supported' 31 | fi 32 | 33 | # woof 34 | printf '%s\n' '# woof()' 35 | woof_function 36 | printf '\n' 37 | 38 | # cd 39 | printf '%s\n' '# cd override' 40 | woof_override_cd "$flag_no_cd" 41 | printf '\n' 42 | 43 | util.path_things 44 | } 45 | 46 | woof_override_cd() { 47 | local flag_no_cd="$1" 48 | 49 | case $shell in 50 | fish) 51 | printf '%s\n' 'function __woof_cd_hook() 52 | woof tool cd-override 53 | end' 54 | if [ "$flag_no_cd" = 'no' ]; then 55 | printf '%s\n' 'function cd 56 | __woof_cd_hook 57 | builtin cd "$@" 58 | end 59 | function pushd 60 | __woof_cd_hook 61 | builtin pushd "$@" 62 | end 63 | function popd 64 | __woof_cd_hook 65 | builtin popd "$@" 66 | end' 67 | fi 68 | ;; 69 | zsh|ksh|bash|sh) 70 | # shellcheck disable=SC2016 71 | printf '%s\n' '__woof_cd_hook() { 72 | eval "$(woof tool cd-override)" 73 | }' 74 | if [ "$flag_no_cd" = 'no' ]; then 75 | printf '%s\n' 'cd() { 76 | __woof_cd_hook 77 | builtin cd "$@" 78 | } 79 | pushd() { 80 | __woof_cd_hook 81 | builtin pushd "$@" 82 | } 83 | popd() { 84 | __woof_cd_hook 85 | builtin popd "$@" 86 | }' 87 | fi 88 | ;; 89 | esac 90 | } 91 | 92 | woof_function() { 93 | printf '%s\n' "woof() { 94 | builtin command woof \"\$@\" 95 | builtin eval \"\$(builtin command woof tool print-eval)\" 96 | builtin hash -r 97 | }" 98 | } 99 | -------------------------------------------------------------------------------- /pkg/src/commands/woof-install.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | woof-install() { 4 | local -a args=() 5 | declare -g g_flag_dry_run='no' 6 | local flag_fetch='no' flag_force='no' 7 | local arg= 8 | for arg; do case $arg in 9 | --help) 10 | util.help_show_usage_and_flags 'install' 11 | util.help_show_part '.install' 12 | exit 0 13 | ;; 14 | --fetch) 15 | flag_fetch='yes' 16 | ;; 17 | --dry-run) 18 | g_flag_dry_run='yes' 19 | ;; 20 | --force) 21 | flag_force='yes' 22 | ;; 23 | -*) 24 | util.print_help_die '.install' "Flag '$arg' not recognized" 25 | ;; 26 | *) 27 | args+=("$arg") 28 | esac done; unset -v arg 29 | 30 | helper.determine_tool_pair_active "${args[0]}" 31 | declare -g g_tool_pair="$REPLY1" 32 | declare -g g_plugin_name="$REPLY2" 33 | declare -g g_tool_name="$REPLY3" 34 | 35 | helper.create_version_table "$flag_fetch" 36 | 37 | helper.determine_tool_version_active --allow-latest "${args[1]}" 38 | declare -g g_tool_version="$REPLY" 39 | 40 | local flag_interactive='no' 41 | helper.install_tool_version "$flag_interactive" "$flag_force" 42 | 43 | util.tool_get_global_version --no-error "$g_tool_pair" 44 | local tool_version_global="$REPLY" 45 | if [ -z "$tool_version_global" ]; then 46 | util.tool_set_global_version "$g_tool_pair" "$g_tool_version" 47 | fi 48 | } 49 | -------------------------------------------------------------------------------- /pkg/src/commands/woof-list.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | woof-list() { 4 | local -a plugins=() 5 | local flag_global='no' flag_fetch='no' flag_all='no' 6 | local arg= 7 | for arg; do case $arg in 8 | --help) 9 | util.help_show_usage_and_flags 'list' 10 | util.help_show_part '.list' 11 | exit 0 12 | ;; 13 | --global) 14 | flag_global='yes' 15 | ;; 16 | --fetch) 17 | flag_fetch='yes' 18 | ;; 19 | --all) 20 | flag_all='yes' 21 | ;; 22 | -*) 23 | util.print_help_die '.list' "Flag '$arg' not recognized" 24 | ;; 25 | *) 26 | plugins+=("$arg") 27 | esac done; unset -v arg 28 | 29 | if [[ "$flag_fetch" = 'yes' && "$flag_all" = 'no' ]]; then 30 | util.print_error_die "Flag --fetch must only be used with --all" 31 | fi 32 | 33 | if [[ "$flag_all" = 'yes' && "${#plugins[@]}" -gt 0 ]]; then 34 | util.print_error_die "Cannot pass in plugins if passing in '--all'" 35 | fi 36 | 37 | if [ "$flag_global" = 'yes' ]; then 38 | util.tool_list_global_versions "$flag_fetch" "$flag_all" "${plugins[@]}" 39 | else 40 | util.tool_list_local_versions "$flag_fetch" "$flag_all" "${plugins[@]}" 41 | fi 42 | 43 | } 44 | -------------------------------------------------------------------------------- /pkg/src/commands/woof-plugin-disable.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | woof-plugin-disable() { 4 | local -a args=() 5 | local arg= 6 | for arg; do case $arg in 7 | --help) 8 | util.help_show_usage_and_flags 'plugin disable' 9 | util.help_show_part '.plugin.disable' 10 | exit 0 11 | ;; 12 | -*) 13 | util.print_help_die '.plugin.disable' "Flag '$arg' not recognized" 14 | ;; 15 | *) 16 | args+=("$arg") 17 | esac done; unset -v arg 18 | 19 | helper.plugin_disable "${args[@]}" 20 | } 21 | -------------------------------------------------------------------------------- /pkg/src/commands/woof-plugin-enable.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | woof-plugin-enable() { 4 | local -a plugins=() 5 | local arg= 6 | for arg; do case $arg in 7 | --help) 8 | util.help_show_usage_and_flags 'plugin enable' 9 | util.help_show_part '.plugin.enable' 10 | exit 0 11 | ;; 12 | -*) 13 | util.print_help_die '.plugin.enable' "Flag '$arg' not recognized" 14 | ;; 15 | *) 16 | plugins+=("$arg") 17 | esac done; unset -v arg 18 | 19 | helper.plugin_enable "${plugins[@]}" 20 | } 21 | -------------------------------------------------------------------------------- /pkg/src/commands/woof-plugin-info.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | woof-plugin-info() { 4 | local -a args=() 5 | local arg= 6 | for arg; do case $arg in 7 | --help) 8 | util.help_show_usage_and_flags 'plugin info' 9 | util.help_show_part '.plugin.info' 10 | exit 0 11 | ;; 12 | -*) 13 | util.print_help_die '.plugin.info' "Flag '$arg' not recognized" 14 | ;; 15 | *) 16 | args+=("$arg") 17 | esac done; unset -v arg 18 | 19 | helper.plugin_info "${args[@]}" 20 | } 21 | -------------------------------------------------------------------------------- /pkg/src/commands/woof-plugin-install.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | woof-plugin-install() { 4 | local -a plugins=() 5 | local flag_force='no' 6 | local arg= 7 | for arg; do case $arg in 8 | --help) 9 | util.help_show_usage_and_flags 'plugin install' 10 | util.help_show_part '.plugin.install' 11 | exit 0 12 | ;; 13 | --force) 14 | flag_force='yes' 15 | ;; 16 | -*) 17 | util.print_help_die '.plugin.install' "Flag '$arg' not recognized" 18 | ;; 19 | *) 20 | plugins+=("$arg") 21 | esac done; unset -v arg 22 | 23 | helper.plugin_install "$flag_force" "${plugins[@]}" 24 | } 25 | -------------------------------------------------------------------------------- /pkg/src/commands/woof-plugin-list.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | woof-plugin-list() { 4 | local -a args=() 5 | local arg= 6 | for arg; do case $arg in 7 | --help) 8 | util.help_show_usage_and_flags 'plugin list' 9 | util.help_show_part '.plugin.list' 10 | exit 0 11 | ;; 12 | -*) 13 | util.print_help_die '.plugin.list' "Flag '$arg' not recognized" 14 | ;; 15 | *) 16 | args+=("$arg") 17 | ;; 18 | esac done; unset -v arg 19 | 20 | helper.plugin_list 21 | } 22 | -------------------------------------------------------------------------------- /pkg/src/commands/woof-plugin-uninstall.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | woof-plugin-uninstall() { 4 | local -a plugins=() 5 | local arg= 6 | for arg; do case $arg in 7 | --help) 8 | util.help_show_usage_and_flags 'plugin uninstall' 9 | util.help_show_part '.plugin.uninstall' 10 | exit 0 11 | ;; 12 | -*) 13 | util.print_help_die '.plugin.uninstall' "Flag '$arg' not recognized" 14 | ;; 15 | *) 16 | plugins+=("$arg") 17 | esac done; unset -v arg 18 | 19 | helper.plugin_uninstall "${plugins[@]}" 20 | } 21 | -------------------------------------------------------------------------------- /pkg/src/commands/woof-plugin.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | woof-plugin() { 4 | local arg= 5 | for arg; do case $arg in 6 | --help) 7 | util.help_show_cmd_plugin_all 8 | exit 0 9 | ;; 10 | -*) 11 | util.print_help_die '.plugin' "Flag '$arg' not recognized" 12 | ;; 13 | *) 14 | break 15 | esac done; unset -v arg 16 | 17 | local subcommand="$1" 18 | if [ -z "$subcommand" ]; then 19 | util.help_show_cmd_plugin_all 20 | util.print_error_die 'No subcommand was given' 21 | fi 22 | if ! shift; then 23 | util.print_fatal_die 'Failed to shift' 24 | fi 25 | # shellcheck disable=SC1090 26 | case $subcommand in 27 | install) 28 | source "$BASALT_PACKAGE_DIR/pkg/src/commands/woof-plugin-$subcommand.sh" 29 | woof-plugin-install "$@" 30 | ;; 31 | uninstall) 32 | source "$BASALT_PACKAGE_DIR/pkg/src/commands/woof-plugin-$subcommand.sh" 33 | woof-plugin-uninstall "$@" 34 | ;; 35 | enable) 36 | source "$BASALT_PACKAGE_DIR/pkg/src/commands/woof-plugin-$subcommand.sh" 37 | woof-plugin-enable "$@" 38 | ;; 39 | disable) 40 | source "$BASALT_PACKAGE_DIR/pkg/src/commands/woof-plugin-$subcommand.sh" 41 | woof-plugin-disable "$@" 42 | ;; 43 | info) 44 | source "$BASALT_PACKAGE_DIR/pkg/src/commands/woof-plugin-$subcommand.sh" 45 | woof-plugin-info "$@" 46 | ;; 47 | list) 48 | source "$BASALT_PACKAGE_DIR/pkg/src/commands/woof-plugin-$subcommand.sh" 49 | woof-plugin-list "$@" 50 | ;; 51 | *) util.print_error_die "Plugin subcommand '$subcommand' not recognized" ;; 52 | esac 53 | } 54 | -------------------------------------------------------------------------------- /pkg/src/commands/woof-set-version.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | woof-set-version() { 4 | local -a args=() 5 | local flag_global='no' 6 | local arg= 7 | for arg; do case $arg in 8 | --help) 9 | util.help_show_usage_and_flags 'set-version' 10 | util.help_show_part '.set-version' 11 | exit 0 12 | ;; 13 | --global) 14 | flag_global='yes' 15 | ;; 16 | -*) 17 | util.print_help_die '.set-version' "Flag '$arg' not recognized" 18 | ;; 19 | *) 20 | args+=("$arg") 21 | esac done; unset -v arg 22 | 23 | helper.determine_tool_pair_active "${args[0]}" 24 | declare -g g_tool_pair="$REPLY1" 25 | declare -g g_plugin_name="$REPLY2" 26 | declare -g g_tool_name="$REPLY3" 27 | 28 | helper.determine_tool_version_installed "$g_tool_pair" "${args[1]}" 29 | local g_tool_version="$REPLY" 30 | 31 | if [ "$flag_global" = 'yes' ]; then 32 | util.tool_set_global_version "$g_tool_pair" "$g_tool_version" 33 | else 34 | util.tool_set_local_version "$g_tool_pair" "$g_tool_version" 35 | 36 | fi 37 | } 38 | -------------------------------------------------------------------------------- /pkg/src/commands/woof-tool.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | woof-tool() { 4 | local -a args=() 5 | local arg= 6 | for arg; do case $arg in 7 | --help) 8 | util.help_show_cmd_tool_all 9 | exit 0 10 | ;; 11 | -*) 12 | util.print_help_die '.tool' "Flag '$arg' not recognized" 13 | ;; 14 | *) 15 | args+=("$arg") 16 | esac done; unset -v arg 17 | 18 | local subcmd="$1" 19 | if [ -z "$subcmd" ]; then 20 | util.help_show_cmd_tool_all 21 | util.print_error_die 'No subcommand was given' 22 | fi 23 | if ! shift; then 24 | util.print_fatal_die 'Failed to shift' 25 | fi 26 | 27 | if [ "$subcmd" = 'get-exe' ]; then 28 | local cmd="${args[1]}" 29 | if [ -z "$cmd" ]; then 30 | util.print_error_die 'Failed to supply command' 31 | return 32 | fi 33 | 34 | helper.toolversions_get_executable_safe "$cmd" 35 | elif [ "$subcmd" = 'print-dirs' ]; then 36 | local var_name= 37 | for var_name in $WOOF_VARS; do 38 | local -n var_value="$var_name" 39 | 40 | printf '%s\n' "---------- $var_name ----------" 41 | if [ -d "$var_value" ]; then 42 | tree -aL 2 --filelimit 15 --noreport "$var_value" 43 | else 44 | term.style_italic -Pd 'Does not exist' 45 | fi 46 | printf '\n' 47 | done; unset -v var_name 48 | unset -vn var_value 49 | elif [ "$subcmd" = 'debug-table' ]; then 50 | helper.determine_tool_pair_active "$1" 51 | local tool_name="$REPLY3" 52 | 53 | util.run_function "$tool_name.table" 54 | elif [ "$subcmd" = 'debug-install' ]; then 55 | helper.determine_tool_pair_active "$1" 56 | declare -g g_tool_pair="$REPLY1" 57 | declare -g g_plugin_name="$REPLY2" 58 | declare -g g_tool_name="$REPLY3" 59 | 60 | helper.create_version_table "$g_tool_pair" 'yes' 61 | 62 | helper.determine_tool_version_active "$2" 63 | local g_tool_version="$REPLY" 64 | 65 | local flag_interactive='yes' 66 | local flag_force='yes' 67 | helper.install_tool_version "$flag_interactive" "$flag_force" "$g_tool_pair" "$g_tool_version" 68 | elif [ "$subcmd" = 'dev-release' ]; then 69 | if [ ! -f 'manifest.ini' ]; then 70 | util.print_error_die "Failed to find file in current directory: 'manifest.ini'" 71 | fi 72 | 73 | if [ ! -d '.git' ]; then 74 | util.print_error_die "Current directory is not a Git repository" 75 | fi 76 | 77 | local output= 78 | if ! output=$(git status --porcelain); then 79 | util.print_error_die "Failed to run 'git status - -porcelain'" 80 | fi 81 | if [ -n "$output" ]; then 82 | util.print_error_die "Aborting because your working directory is dirty" 83 | fi 84 | unset -v output 85 | 86 | if ! grep -q 'version *= *' 'manifest.ini'; then 87 | util.print_error_die "Failed to find a 'version' key in manifest file" 88 | fi 89 | 90 | local current_version= 91 | current_version=$(grep 'version *= *' 'manifest.ini' | cut -d= -f2) 92 | current_version=${current_version#"${current_version%%[![:space:]]*}"} 93 | current_version=${current_version%"${current_version##*[![:space:]]}"} 94 | printf '%s\n' "current_version: $current_version" 95 | 96 | local new_version= 97 | read -rp 'New Version: ' new_version 98 | 99 | if [[ $new_version == v* ]]; then 100 | util.print_error_die "New version should not be prefixed with a 'v'" 101 | fi 102 | 103 | sed -Ei'' "s/(version[\\t ]*=[\\t ])(.*)[\\t ]*/\\1$new_version/g" 'manifest.ini' 104 | 105 | git add 'manifest.ini' 106 | git commit -nm "release: v$new_version" 107 | git tag -a -m "v$new_version" "v$new_version" 108 | elif [ "$subcmd" = 'clear-table-cache' ]; then 109 | local tool_pair="$1" 110 | 111 | var.get_plugin_table_file "$g_tool_pair" 112 | local table_file="$REPLY" 113 | 114 | if [ -z "$g_tool_pair" ]; then 115 | util.print_info "Removing all table cache" 116 | # Since '$tool_pair' is empty, the basename of '$table_file' is 117 | # not correct, but that doesn't matter as it is not used here 118 | rm -rf "${table_file%/*}" 119 | else 120 | util.print_info "Removing table cache for '$tool_pair'" 121 | rm -f "$table_file" 122 | fi 123 | elif [ "$subcmd" = 'cd-override' ]; then 124 | util.toolversions_get_file 125 | local toolversions_file="$REPLY" 126 | if [ -n "$toolversions_file" ]; then 127 | helper.toolversions_set_versions "$toolversions_file" 128 | fi 129 | util.path_things 130 | elif [ "$subcmd" = 'print-eval' ]; then 131 | util.path_things 132 | else 133 | util.print_error_die "Subcommand '$subcmd' is not valid" 134 | fi 135 | } 136 | -------------------------------------------------------------------------------- /pkg/src/commands/woof-uninstall.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | woof-uninstall() { 4 | local -a args=() 5 | local arg= 6 | for arg; do case $arg in 7 | --help) 8 | util.help_show_usage_and_flags 'uninstall' 9 | util.help_show_part '.uninstall' 10 | exit 0 11 | ;; 12 | -*) 13 | util.print_help_die '.uninstall.' "Flag '$arg' not recognized" 14 | ;; 15 | *) 16 | args+=("$arg") 17 | esac done; unset -v arg 18 | 19 | helper.determine_tool_pair_installed "${args[0]}" 20 | declare -g g_tool_pair="$REPLY1" 21 | declare -g g_plugin_name="$REPLY2" 22 | declare -g g_tool_name="$REPLY3" 23 | 24 | helper.determine_tool_version_installed "$g_tool_pair" "${args[1]}" 25 | declare -g g_tool_version="$REPLY" 26 | 27 | var.get_dir 'tools' "$g_tool_pair" 28 | local install_dir="$REPLY" 29 | 30 | # Do uninstall 31 | printf '%s\n' "Uninstalling $g_tool_pair" 32 | # Note that this is a redundant check since it is done by helper.determine_tool_version_installed(), but we 33 | # do it anyways, Just in Case 34 | if [ -e "$install_dir/$g_tool_version" ]; then 35 | rm -rf "${install_dir:?}/$g_tool_version" 36 | util.print_info "Removed version '$g_tool_version' for plugin '$g_tool_pair'" 37 | else 38 | util.print_error_die "Version '$g_tool_version' for plugin '$g_tool_pair' is not installed" 39 | fi 40 | 41 | # Remove the selected tool version if it was just uninstalled. 42 | var.get_dir 'data' 43 | local dir="$REPLY/selection" 44 | local selection_file="$dir/$g_tool_pair" 45 | local selection= 46 | selection=$(<"$selection_file") 47 | if [ "$selection" = "$g_tool_version" ]; then 48 | rm "$selection_file" 49 | core.print_warn "You just removed the Go version that was selected by default." 50 | fi 51 | } 52 | -------------------------------------------------------------------------------- /pkg/src/filter_utils/util/util.jq: -------------------------------------------------------------------------------- 1 | def die(msg): "Error: " + msg | halt_error; 2 | 3 | def print_error(msg): "Error: " + msg | debug | empty; 4 | 5 | def filter_github(content; num): "Something: " + content + num | tostring; 6 | 7 | def specialcase_lean_arch: "x86_64"; 8 | -------------------------------------------------------------------------------- /pkg/src/filter_utils/util/util.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | echo sourced 3 | 4 | f.is_main() { 5 | if [ "${BASH_SOURCE[0]}" != "$0" ]; then :; else 6 | return $? 7 | fi 8 | } 9 | 10 | f.die() { 11 | local msg="$1" 12 | 13 | printf '%s\n' "Error: $msg. Exiting" >&2 14 | exit 1 15 | } 16 | 17 | f.print_error() { 18 | printf '%s\n' "Error: %s" >&2 19 | } 20 | -------------------------------------------------------------------------------- /pkg/src/util/helper-determine.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | helper.determine_tool_pair_active() { 4 | unset -v REPLY; REPLY= 5 | local input="$1" 6 | 7 | util.determine_tool_pair "$input" 'active' 8 | local tool_pair=$REPLY1 9 | local plugin_name=$REPLY2 10 | local tool_name=$REPLY3 11 | 12 | var.get_tool_file "$plugin_name" "$tool_name" 13 | local tool_file="$REPLY" 14 | 15 | if [ ! -f "$tool_file" ]; then 16 | util.print_error_die "Tool '$input' not found" 17 | fi 18 | 19 | # shellcheck source=/dev/null 20 | if ! source "$tool_file"; then # TODO: REMOVE THIS 21 | util.print_error_die "Could not successfully source plugin '$tool_pair'" 22 | fi 23 | 24 | REPLY1=$tool_pair 25 | REPLY2=$plugin_name 26 | REPLY3=$tool_name 27 | } 28 | 29 | helper.determine_tool_pair_installed() { 30 | unset -v REPLY; REPLY= 31 | local input="$1" 32 | 33 | util.determine_tool_pair "$input" 'installed' 34 | local tool_pair=$REPLY1 35 | local plugin_name=$REPLY2 36 | local tool_name=$REPLY3 37 | 38 | var.get_dir 'tools' "$tool_pair" 39 | local install_dir="$REPLY" 40 | 41 | if [ ! -d "$install_dir" ]; then 42 | util.print_error_die "No versions of plugin '$tool_pair' are installed" 43 | fi 44 | 45 | REPLY1=$tool_pair 46 | REPLY2=$plugin_name 47 | REPLY3=$tool_name 48 | } 49 | 50 | helper.determine_tool_version_active() { 51 | unset -v REPLY; REPLY= 52 | local flag_allow_latest='no' 53 | if [ "$1" = '--allow-latest' ]; then 54 | flag_allow_latest='yes' 55 | if ! shift; then 56 | print.panic 'Failed to shift' 57 | fi 58 | fi 59 | local tool_version="$1" 60 | 61 | util.uname_system 62 | local real_os="$REPLY1" 63 | local real_arch="$REPLY2" 64 | 65 | if [[ "$flag_allow_latest" = 'yes' && "$tool_version" = 'latest' ]]; then 66 | util.get_latest_tool_version "$g_tool_pair" "$real_os" "$real_arch" 67 | tool_version="$REPLY" 68 | fi 69 | 70 | var.get_plugin_table_file "$g_tool_pair" 71 | local table_file="$REPLY" 72 | 73 | if [ -z "$tool_version" ]; then 74 | local -a ui_keys=() 75 | local -A ui_table=() 76 | 77 | local match_found='no' 78 | local variant= version= os= arch= url= comment= 79 | while IFS='|' read -r variant version os arch url comment; do 80 | if [ "$real_os" = "$os" ] && [ "$real_arch" = "$arch" ]; then 81 | match_found='yes' 82 | ui_keys+=("$version") 83 | ui_table["$version"]="$url $comment" 84 | fi 85 | done < "$table_file"; unset -v version os arch url comment 86 | 87 | if [ "$match_found" != 'yes' ]; then 88 | util.print_error_die "Could not find any matching versions for the current os/arch" 89 | fi 90 | 91 | util.tool_get_global_version --no-error "$g_tool_pair" 92 | local tool_version_global="$REPLY" 93 | 94 | tty.multiselect "$tool_version_global" ui_keys ui_table 95 | tool_version="$REPLY" 96 | fi 97 | 98 | local is_valid_string='yes' 99 | local variant= version= os= arch= url= comment= 100 | while IFS='|' read -r variant version os arch url comment; do 101 | if [ "$tool_version" = "$version" ] && [ "$real_os" = "$os" ] && [ "$real_arch" = "$arch" ]; then 102 | is_valid_string='yes' 103 | fi 104 | done < "$table_file"; unset -v variant version os arch url comment 105 | 106 | if [ "$is_valid_string" != yes ]; then 107 | util.print_error_die "Version '$tool_version' is not valid for plugin '$g_tool_pair' on this architecture" 108 | fi 109 | 110 | REPLY=$tool_version 111 | } 112 | 113 | # @description Get the installed version string, if one was not already specified 114 | helper.determine_tool_version_installed() { 115 | local tool_pair="$1" 116 | local tool_version="$2" 117 | 118 | var.get_dir 'tools' "$g_tool_pair" 119 | local install_dir="$REPLY" 120 | 121 | if [ -z "$tool_version" ]; then 122 | core.shopt_push -s nullglob 123 | local -a versions_list=("$install_dir"/*/) 124 | core.shopt_pop 125 | 126 | if (( ${#versions_list[@]} == 0 )); then 127 | util.print_error_die "Cannot uninstall as no versions of plugin '$g_tool_pair' are installed" 128 | fi 129 | 130 | versions_list=("${versions_list[@]%/}") 131 | versions_list=("${versions_list[@]##*/}") 132 | 133 | local -A versions_table=() 134 | local version= 135 | for version in "${versions_list[@]}"; do 136 | versions_table["$version"]= 137 | done; unset -v version 138 | 139 | util.tool_get_global_version --no-error "$g_tool_pair" 140 | local tool_version_global="$REPLY" 141 | 142 | tty.multiselect "$tool_version_global" versions_list versions_table 143 | tool_version="$REPLY" 144 | fi 145 | 146 | if [ ! -d "$install_dir/$tool_version" ]; then 147 | util.print_error_die "Version '$tool_version' is not valid for plugin '$g_tool_pair'" 148 | fi 149 | 150 | REPLY="$tool_version" 151 | } 152 | 153 | helper.determine_plugin() { 154 | local plugin_name="$1" 155 | 156 | if [ -z "$plugin_name" ]; then 157 | util.plugin_get_plugins --filter=none --with=name 158 | 159 | local -a plugins_list=("${REPLY[@]}") 160 | local -A plugins_table=() 161 | local plugin= 162 | for plugin in "${plugins_list[@]}"; do 163 | plugins_table["$plugin"]= 164 | done; unset -v plugin 165 | 166 | tty.multiselect '' plugins_list plugins_table 167 | plugin_name=$REPLY 168 | fi 169 | 170 | var.get_dir 'plugins' 171 | local plugins_dir="$REPLY" 172 | 173 | local dir="$plugins_dir/woof-plugin-$plugin_name" 174 | if [ ! -d "$dir" ]; then 175 | util.print_error_die "Plugin '$plugin_name' is not valid (non-existent directory: $dir)" 176 | fi 177 | 178 | unset -v REPLY; REPLY= 179 | REPLY=$plugin_name 180 | } 181 | 182 | helper.determine_plugin_uri() { 183 | local plugin_uri="$1" 184 | 185 | if [ -z "$plugin_uri" ]; then 186 | local -a plugins_list=( 187 | 'github.com/version-manager/woof-plugin-core' 188 | 'github.com/version-manager/woof-plugin-ancillary' 189 | 'github.com/version-manager/woof-plugin-hashicorp' 190 | ) 191 | local -A plugins_table=() 192 | local plugin= 193 | for plugin in "${plugins_list[@]}"; do 194 | plugins_table["$plugin"]= 195 | done; unset -v plugin 196 | 197 | tty.multiselect '' plugins_list plugins_table 198 | plugin_uri=$REPLY 199 | fi 200 | 201 | unset -v REPLY; REPLY= 202 | REPLY=$plugin_uri 203 | } 204 | -------------------------------------------------------------------------------- /pkg/src/util/helper-plugin.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | # @description Installs a particular plugin 4 | helper.plugin_install() { 5 | util.plugin_prune 6 | 7 | local flag_force="$1" 8 | if ! shift; then 9 | util.print_fatal_die 'Failed to shift' 10 | fi 11 | 12 | local plugin_uris=() 13 | if [ -z "$1" ]; then 14 | helper.determine_plugin_uri "$plugin_name" 15 | plugin_uris=("$REPLY") 16 | else 17 | plugin_uris=("$@") 18 | fi 19 | 20 | local plugin_uri= 21 | for plugin_uri in "${plugin_uris[@]}"; do 22 | # If a local path was not specified 23 | if [[ "$plugin_uri" == /* ]]; then 24 | : 25 | elif [[ "$plugin_uri" == .* ]]; then 26 | plugin_uri="$PWD/$plugin_uri" 27 | else 28 | if [[ "$plugin_uri" == https://* || "$plugin_uri" == http:// ]]; then 29 | : 30 | elif [[ $plugin_uri == */*/* ]]; then 31 | plugin_uri="https://$plugin_uri" 32 | elif [[ "$plugin_uri" == */* ]]; then 33 | plugin_uri="https://github.com/$plugin_uri" 34 | elif [[ "$plugin_uri" == *-* ]]; then 35 | plugin_uri="https://github.com/version-manager/$plugin_uri" 36 | else 37 | plugin_uri="https://github.com/version-manager/woof-plugin-$plugin_uri" 38 | fi 39 | fi 40 | 41 | util.plugin_resolve_external_path "$plugin_uri" 42 | local plugin_type="$REPLY_TYPE" 43 | local plugin_src="$REPLY_SRC" 44 | local plugin_target="$REPLY_TARGET" 45 | 46 | if [ "$plugin_type" = 'symlink' ]; then 47 | util.plugin_assert_is_valid "$plugin_src" 48 | 49 | if [ "$flag_force" = 'no' ]; then 50 | if [ -d "$plugin_target" ]; then 51 | core.print_warn "Plugin is already installed: $plugin_target" 52 | continue 53 | fi 54 | fi 55 | 56 | util.mkdirp "${plugin_target%/*}" 57 | if ln -sfT "$plugin_src" "$plugin_target"; then :; else 58 | util.print_error_die "Failed to symlink plugin: $plugin_src" 59 | fi 60 | elif [ "$plugin_type" = 'git-repository' ]; then 61 | if [ "$flag_force" = 'no' ]; then 62 | if [ -d "$plugin_target" ]; then 63 | core.print_warn "Plugin is already installed: $plugin_target" 64 | continue 65 | fi 66 | fi 67 | 68 | util.print_info "Cloning repository: $plugin_src" 69 | util.mkdirp "${plugin_target%/*}" 70 | if git clone "$plugin_src" "$plugin_target"; then :; else 71 | util.print_error_die "Failed to clone Git repository" 72 | fi 73 | util.print_info "Installed plugin to: $plugin_target" 74 | 75 | util.plugin_assert_is_valid "$plugin_target" 76 | else 77 | util.print_error_die "Failed to recognize plugin type: '$plugin_type'" 78 | fi 79 | done; unset -v plugin_uri 80 | } 81 | 82 | # @description Uninstalls a particular set of plugins 83 | helper.plugin_uninstall() { 84 | util.plugin_prune 85 | 86 | local plugin_names=() 87 | 88 | if [ -z "$1" ]; then 89 | helper.determine_plugin "$plugin_name" 90 | plugin_names=("$REPLY") 91 | else 92 | plugin_names=("$@") 93 | fi 94 | 95 | var.get_dir 'plugins' 96 | local plugins_dir="$REPLY" 97 | 98 | local plugin_name= 99 | for plugin_name in "${plugin_names[@]}"; do 100 | local plugin_dir="$plugins_dir/woof-plugin-$plugin_name" 101 | 102 | if [ -L "$plugin_dir" ]; then 103 | unlink "$plugin_dir" 104 | core.print_info "Unlinked plugin: $plugin_name" 105 | else 106 | rm -rf "${plugin_dir?:}" 107 | core.print_info "Uninstalled plugin: $plugin_name" 108 | fi 109 | done; unset -v plugin_name 110 | } 111 | 112 | # @description Enables a particular set of plugins 113 | helper.plugin_enable() { 114 | util.plugin_prune 115 | 116 | local plugin_names=() 117 | 118 | if [ -z "$1" ]; then 119 | helper.determine_plugin "$plugin_name" 120 | plugin_names=("$REPLY") 121 | else 122 | plugin_names=("$@") 123 | fi 124 | 125 | local plugin_name= 126 | for plugin_name in "${plugin_names[@]}"; do 127 | if util.plugin_is_enabled "$plugin_name"; then 128 | core.print_warn "Plugin already enabled: $plugin_name" 129 | else 130 | util.plugin_set_enabled "$plugin_name" 131 | util.print_info "Enabled plugin: $plugin_name" 132 | fi 133 | done; unset -v plugin_name 134 | } 135 | 136 | # @description Disables a particular set of plugins 137 | helper.plugin_disable() { 138 | util.plugin_prune 139 | 140 | local plugin_names=() 141 | 142 | if [ -z "$1" ]; then 143 | helper.determine_plugin "$plugin_name" 144 | plugin_names=("$REPLY") 145 | else 146 | plugin_names=("$@") 147 | fi 148 | 149 | local plugin_name= 150 | for plugin_name in "${plugin_names[@]}"; do 151 | if util.plugin_is_enabled "$plugin_name"; then 152 | util.plugin_set_disabled "$plugin_name" 153 | util.print_info "Disabled plugin: $plugin_name" 154 | else 155 | core.print_warn "Plugin already disabled: $plugin_name" 156 | fi 157 | done; unset -v plugin_name 158 | } 159 | 160 | # @description Prints information about a set of plugins 161 | helper.plugin_info() { 162 | util.plugin_prune 163 | 164 | local plugin_names=() 165 | 166 | if [ -z "$1" ]; then 167 | helper.determine_plugin "$plugin_name" 168 | plugin_names=("$REPLY") 169 | else 170 | plugin_names=("$@") 171 | fi 172 | 173 | var.get_dir 'plugins' 174 | local plugins_dir="$REPLY" 175 | 176 | local plugin_name= 177 | for plugin_name in "${plugin_names[@]}"; do 178 | local plugin_dir="$plugins_dir/woof-plugin-$plugin_name" 179 | 180 | util.plugin_show_one "$plugin_dir" 181 | done; unset -v plugin_name 182 | } 183 | 184 | # @description Prints information about all plugins 185 | helper.plugin_list() { 186 | util.plugin_prune 187 | 188 | util.plugin_get_plugins --filter=none --with=filepath 189 | if ((${#REPLY[@]} == 0)); then 190 | term.style_italic -Pd 'No plugins installed' >&2 191 | return 0 192 | fi 193 | 194 | local plugin_dir 195 | for plugin_dir in "${REPLY[@]}"; do 196 | util.plugin_show_one "$plugin_dir" 197 | done; unset -v plugin_dir 198 | } 199 | -------------------------------------------------------------------------------- /pkg/src/util/helper-tool.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | # TODO: rename 3 | 4 | util.tool_get_global_version() { 5 | local arg= flag_no_error='no' 6 | for arg; do case $arg in 7 | --no-error) 8 | flag_no_error='yes' 9 | if ! shift; then 10 | util.print_fatal_die 'Failed to shift' 11 | exit 1 12 | fi 13 | ;; 14 | esac done; unset -v arg 15 | 16 | local tool_pair="$1" 17 | 18 | var.get_dir 'data' 19 | local dir="$REPLY/selection" 20 | 21 | unset -v REPLY; REPLY= 22 | 23 | if [ -f "$dir/$tool_pair" ]; then 24 | REPLY=$(<"$dir/$tool_pair") 25 | return 26 | else 27 | if [ "$flag_no_error" = 'yes' ]; then 28 | return 29 | else 30 | util.print_error_die "A global version of '$tool_pair' has not been set" 31 | fi 32 | fi 33 | } 34 | 35 | util.tool_get_local_version() { 36 | local arg= flag_no_error='no' 37 | for arg; do case $arg in 38 | --no-error) 39 | flag_no_error='yes' 40 | if ! shift; then 41 | util.print_fatal_die 'Failed to shift' 42 | exit 1 43 | fi 44 | ;; 45 | esac done; unset -v arg 46 | 47 | local tool_pair="$1" 48 | 49 | var.get_dir 'data' 50 | local dir="$REPLY/selection" 51 | 52 | unset -v REPLY; REPLY= 53 | 54 | if [ ! -f "$dir/$tool_pair" ]; then 55 | if [ "$flag_no_error" = 'yes' ]; then 56 | return 57 | else 58 | util.print_error_die "No default was found for plugin '$tool_pair'" 59 | fi 60 | fi 61 | 62 | unset -v REPLY; REPLY= 63 | REPLY=$(<"$dir/$tool_pair") 64 | } 65 | 66 | util.tool_set_global_version() { 67 | var.get_dir 'data' 68 | local dir="$REPLY/selection" 69 | 70 | util.mkdirp "$dir/$g_plugin_name" 71 | 72 | if ! printf '%s\n' "$g_tool_version" > "$dir/$g_tool_pair"; then 73 | util.print_error_die "Failed to write new global version to disk" 74 | fi 75 | 76 | util.print_info "Set version '$g_tool_version' as global version" 77 | } 78 | 79 | 80 | util.tool_set_local_version() { 81 | var.get_dir 'data' 82 | local dir="$REPLY/selection" 83 | 84 | util.mkdirp "$dir/$g_plugin_name" 85 | 86 | if ! printf '%s\n' "$g_tool_version" > "$dir/$g_tool_pair"; then 87 | util.print_error_die "Failed to write new local version to disk" 88 | fi 89 | 90 | util.print_info "Set version '$g_tool_version' as local version" 91 | } 92 | 93 | util.tool_list_global_versions() { 94 | local flag_fetch="$1" 95 | local flag_all="$2" 96 | if ! shift 2; then 97 | util.print_fatal_die 'Failed to shift' 98 | exit 1 99 | fi 100 | 101 | # TODO 102 | # if (( ${#versions[@]} == 0)); then 103 | # term.style_italic -Pd 'No items' >&2 104 | # return 105 | # fi 106 | 107 | if [ "$flag_all" = 'yes' ]; then 108 | var.get_dir 'tools' "$tool_pair" 109 | local install_dir="$REPLY" 110 | 111 | # TODO: show ones that are 'done' and not 'done' 112 | local tool= 113 | for tool in "$install_dir"/*/; do 114 | tool=${tool%/} 115 | tool=${tool##*/} 116 | local tool_pair="$tool" 117 | 118 | # one shoudl already be created / should not do this in list 119 | # as it could mean network request TODO 120 | helper.create_version_table "$flag_fetch" 121 | 122 | printf '%s\n' "$tool_pair" 123 | 124 | util.uname_system 125 | local real_os="$REPLY1" 126 | local real_arch="$REPLY2" 127 | 128 | var.get_plugin_table_file "$tool_pair" 129 | local table_file="$REPLY" 130 | 131 | local variant= version= os= arch= url= comment= 132 | while IFS='|' read -r variant version os arch url comment; do 133 | if [ "$real_os" = "$os" ] && [ "$real_arch" = "$arch" ]; then 134 | printf '%s\n' " $version" 135 | fi 136 | done < "$table_file" | util.sort_versions 137 | unset -v variant version os arch url comment 138 | # TODO: auto pager 139 | done 140 | else 141 | var.get_dir 'tools' 142 | local tools_dir="$REPLY" 143 | 144 | if ((${#tools_dir[@]} == 0)); then 145 | term.style_italic -Pd 'No items' >&2 146 | return 147 | fi 148 | 149 | util.get_installed_tools 150 | for tool_dir in "${REPLY[@]}"; do 151 | printf '%s\n' "dir: $tool_dir" 152 | done; unset -v tool_dir 153 | 154 | # helper.determine_tool_pair_active "$1" 155 | # declare -g g_tool_pair="$REPLY" 156 | # declare -g g_plugin_name="$REPLY1" 157 | # declare -g g_tool_name="$REPLY2" 158 | 159 | # # TODO 160 | # # DUPLICATE CODE START ? 161 | # helper.create_version_table "$flag_fetch" 162 | 163 | # printf '%s\n' "$g_tool_pair" 164 | 165 | # util.uname_system 166 | # local real_os="$REPLY1" 167 | # local real_arch="$REPLY2" 168 | 169 | # var.get_plugin_table_file "$g_tool_pair" 170 | # local table_file="$REPLY" 171 | 172 | # local variant= version= os= arch= url= comment= 173 | # while IFS='|' read -r variant version os arch url comment; do 174 | # if [ "$real_os" = "$os" ] && [ "$real_arch" = "$arch" ]; then 175 | # printf '%s\n' " $version" 176 | # fi 177 | # done < "$table_file" | util.sort_versions 178 | # unset -v variant version os arch url comment 179 | # # DUPLICATE CODE END 180 | fi 181 | } 182 | 183 | util.tool_list_local_versions() { 184 | local flag_fetch="$1" 185 | local flag_all="$2" 186 | if ! shift 2; then 187 | util.print_fatal_die 'Failed to shift' 188 | exit 1 189 | fi 190 | 191 | util.toolversions_get_file 192 | local toolversions_file="$REPLY" 193 | 194 | if [ -z "$toolversions_file" ]; then 195 | util.print_error_die 'Local project not found' 196 | fi 197 | 198 | local -A tools=() 199 | util.toolversions_parse "$toolversions_file" 'tools' 200 | 201 | local tool= 202 | for tool in "${!tools[@]}"; do 203 | local -a versions=() 204 | IFS='|' read -r versions <<< "${tools[$tool]}" 205 | 206 | printf '%s\n' "$tool" 207 | local version= 208 | for version in "${versions[@]}"; do 209 | printf '%s\n' " $version" 210 | done 211 | done 212 | } 213 | -------------------------------------------------------------------------------- /pkg/src/util/helper-toolversions.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | fallback() { # TODO 4 | printf '%s\n' "$global_bin/$cmd" 5 | } 6 | 7 | helper.toolversions_get_executable_safe() { 8 | unset -v REPLY; REPLY= 9 | 10 | local cmd="$1" 11 | 12 | util.toolversions_get_file 13 | local toolversions_file="$REPLY" 14 | 15 | if [ -z "$toolversions_file" ]; then 16 | fallback 17 | return 18 | fi 19 | 20 | declare -gA tools=() 21 | util.toolversions_parse "$toolversions_file" 'tools' 22 | 23 | local tool_name= 24 | for tool_name in "${!tools[@]}"; do 25 | # TODO 26 | local translated_cmd="$cmd" 27 | if [[ "$cmd" == @(node|npm|npx) ]]; then 28 | translated_cmd='nodejs' 29 | fi 30 | 31 | if [ "$tool_name" = "$translated_cmd" ]; then 32 | local -a versions=() 33 | IFS='|' read -ra versions <<< "${tools[$tool_name]}" 34 | for tool_version in "${versions[@]}"; do 35 | if [[ $tool_version == ref:* ]]; then 36 | fallback 37 | elif [[ $tool_version == path:* ]]; then 38 | fallback 39 | elif [[ $tool_version == 'system' ]]; then 40 | fallback 41 | else 42 | if util.is_tool_version_installed "$tool_name" "$tool_version"; then 43 | local dir="$REPLY" 44 | printf '%s\n' "$dir/bin/$cmd" 45 | else 46 | # TODO: prefix with Woof 47 | printf '%s\n' "Cannot switch to $tool_name version $tool_version; try to install it first" >&2 48 | fi 49 | fi 50 | done; unset -v tool_version 51 | 52 | return 53 | fi 54 | done; unset -v tool_name 55 | } 56 | 57 | helper.toolversions_set_versions() { 58 | local toolversions_path="$1" 59 | util.assert_not_empty 'toolversions_path' 60 | 61 | declare -gA tools=() 62 | util.toolversions_parse "$toolversions_path" 'tools' 63 | 64 | local tool_name= 65 | for tool_name in "${!tools[@]}"; do 66 | local -a versions=() 67 | IFS='|' read -ra versions <<< "${tools[$tool_name]}" 68 | for tool_version in "${versions[@]}"; do 69 | if [[ $tool_version == ref:* ]]; then 70 | core.print_warn "Skipping '$tool_version' for '$tool_name' as 'ref:' is not yet supported" 71 | elif [[ $tool_version == path:* ]]; then 72 | core.print_warn "Skipping '$tool_version' for '$tool_name' as 'path:' is not yet supported" 73 | elif [[ $tool_version == 'system' ]]; then 74 | core.print_warn "Skipping 'system' for '$tool_name' as 'system' is not yet supported" 75 | else 76 | declare -g g_tool_name="$tool_name" 77 | declare -g g_tool_version="$tool_version" 78 | 79 | if util.is_tool_version_installed "$g_tool_name" "$g_tool_version"; then 80 | util.tool_set_local_version 81 | printf '%s\n' "Switched to $g_tool_name version $g_tool_version" 82 | else 83 | printf '%s\n' "Cannot switch to $g_tool_name version $g_tool_version; try to install it first" 84 | fi 85 | fi 86 | done; unset -v tool_version 87 | done; unset -v tool_name 88 | } 89 | 90 | -------------------------------------------------------------------------------- /pkg/src/util/helper.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | # @description For a given plugin, construct a table of all versions for all 4 | # platforms (kernel and architecture). This eventually calls the ".table" 5 | # function and properly deals with caching 6 | helper.create_version_table() { 7 | local flag_fetch="$1" 8 | util.assert_not_empty 'flag_fetch' 9 | 10 | var.get_plugin_table_file "$g_tool_pair" 11 | local table_file="$REPLY" 12 | 13 | util.print_info 'Gathering versions' 14 | core.print_debug "Table file: $table_file" 15 | 16 | util.mkdirp "${table_file%/*}" 17 | 18 | local should_use_cache='yes' 19 | if [ ! -f "$table_file" ]; then 20 | should_use_cache='no' 21 | fi 22 | if [ "$flag_fetch" = 'yes' ]; then 23 | should_use_cache='no' 24 | fi 25 | 26 | if [ "$should_use_cache" = 'no' ]; then 27 | local table_string= 28 | if table_string=$(WOOF_PLUGIN_NAME=$g_plugin_name util.run_function "$g_tool_name.table"); then 29 | if core.err_exists; then 30 | util.print_error_die "$ERR" 31 | fi 32 | else 33 | util.print_error_die "Failed to run '$g_tool_name.table()'" 34 | fi 35 | 36 | if [ -z "$table_string" ]; then 37 | util.print_error_die "No versions found for $g_tool_name ('$g_tool_name.table()' printed nothing)" 38 | fi 39 | 40 | if ! printf '%s' "$table_string" > "$table_file"; then 41 | rm -f "$table_file" 42 | util.print_error_die "Could not write to '$table_file'" 43 | fi 44 | 45 | unset -v table_string 46 | fi 47 | } 48 | 49 | helper.install_tool_version() { 50 | local flag_interactive="$1" 51 | local flag_force="$2" 52 | util.assert_not_empty 'flag_interactive' 53 | util.assert_not_empty 'flag_force' 54 | 55 | var.get_plugin_workspace_dir "$g_tool_pair" 56 | local workspace_dir="$REPLY" 57 | 58 | var.get_dir 'tools' "$g_tool_pair" 59 | local install_dir="$REPLY" 60 | 61 | # If there is an interactive flag, then we are debugging the installation 62 | # process. In this case, make the workspace and install directory someplace 63 | # totally different 64 | local interactive_dir= 65 | if [ "$flag_interactive" = 'yes' ]; then 66 | if ! interactive_dir="$(mktemp -d)/woof-interactive-$RANDOM"; then 67 | util.print_error_die 'Failed to mktemp' 68 | fi 69 | workspace_dir="$interactive_dir/workspace_dir" 70 | install_dir="$interactive_dir/install_dir" 71 | fi 72 | 73 | if util.is_tool_version_installed "$g_tool_pair" "$g_tool_version"; then 74 | if [ "$flag_force" = 'yes' ]; then 75 | if rm -rf "${install_dir:?}/$g_tool_version"; then :; else 76 | util.print_error_die "Failed to remove directory: '${install_dir:?}/$g_tool_version'" 77 | fi 78 | else 79 | core.print_warn "Version '$g_tool_version' is already installed for plugin '$g_tool_pair'. Switching to that version" 80 | # TODO: global only thing 81 | util.tool_set_global_version "$g_tool_pair" "$g_tool_version" 82 | return 83 | fi 84 | fi 85 | 86 | util.uname_system 87 | local os="$REPLY1" 88 | local arch="$REPLY2" 89 | 90 | # Determine correct binary for current system 91 | util.get_table_row "$g_tool_name" "$g_tool_version" "$os" "$arch" 92 | local url="$REPLY1" 93 | 94 | # Preparation actions 95 | rm -rf "$workspace_dir" "${install_dir:?}/$g_tool_version" 96 | mkdir -p "$workspace_dir" "$install_dir" 97 | 98 | # Execute '.install' 99 | local old_pwd="$PWD" 100 | if ! cd -- "$workspace_dir"; then 101 | util.print_error_die 'Failed to cd' 102 | fi 103 | core.print_debug "Working directory changed to: $PWD" 104 | 105 | unset -v REPLY_DIR 106 | unset -v REPLY_{BINS,INCLUDES,LIBS,MANS} REPLY_{BASH,ZSH,FISH}_COMPLETIONS 107 | declare -g REPLY_DIR= 108 | declare -ag REPLY_BINS=() REPLY_INCLUDES=() REPLY_LIBS=() REPLY_MANS=() REPLY_BASH_COMPLETIONS=() \ 109 | REPLY_ZSH_COMPLETIONS=() REPLY_FISH_COMPLETIONS=() 110 | if WOOF_PLUGIN_NAME=$g_plugin_name util.run_function "$g_tool_name.install" "$url" "${g_tool_version/#v}" "$os" "$arch"; then 111 | if core.err_exists; then 112 | rm -rf "$workspace_dir" 113 | util.print_error_die "Failed to successfully execute '${g_tool_pair#*/}.install'" 114 | fi 115 | else 116 | rm -rf "$workspace_dir" 117 | util.print_error_die "Unexpected error while calling '$g_tool_name.install'" 118 | fi 119 | if ! cd -- "$old_pwd"; then 120 | util.print_error_die 'Failed to cd' 121 | fi 122 | 123 | if [ "$flag_interactive" = 'yes' ]; then 124 | util.print_info "Dropping into a shell to interactively debug installation process. Exit shell to continue normally" 125 | if ( 126 | if ! cd -- "$workspace_dir"; then 127 | util.print_error_die 'Failed to cd' 128 | fi 129 | printf '%s\n' "Download URL: $url" 130 | bash 131 | ); then :; else 132 | local exit_code=$? 133 | 134 | rm -rf "$interactive_dir" 135 | exit $exit_code 136 | fi 137 | 138 | rm -rf "$interactive_dir" 139 | fi 140 | 141 | if [ -z "$REPLY_DIR" ]; then 142 | util.print_error_die "Variable '\$REPLY_DIR' must be set at the end of .install" 143 | fi 144 | 145 | # Move extracted contents to 'tools' directory 146 | core.shopt_push -s dotglob 147 | if ! mv "$workspace_dir/$REPLY_DIR" "$install_dir/$g_tool_version"; then 148 | rm -rf "$workspace_dir" 149 | util.print_error_die "Could not move extracted contents to '$install_dir/$g_tool_version'" 150 | fi 151 | core.shopt_pop 152 | 153 | # Save information about bin, man, etc. pages later 154 | mkdir -p "$install_dir/$g_tool_version/.woof_" 155 | local old_ifs="$IFS"; IFS=':' 156 | if ! printf '%s\n' "bins=${REPLY_BINS[*]} 157 | mans=${REPLY_MANS[*]}" > "$install_dir/$g_tool_version/.woof_/data.txt"; then 158 | rm -rf "$workspace_dir" "${install_dir:?}/$g_tool_version" 159 | util.print_error_die "Failed to write to '$install_dir/$g_tool_version/.woof_/data.txt'" 160 | fi 161 | IFS="$old_ifs" 162 | 163 | rm -rf "$workspace_dir" 164 | if [ "$flag_interactive" = 'no' ]; then 165 | mkdir -p "$install_dir/$g_tool_version/.woof_" 166 | : > "$install_dir/$g_tool_version/.woof_/done" 167 | util.print_info "Installed $g_tool_version" 168 | else 169 | util.print_info "Exiting interactive environment. Intermediate temporary directories have been deleted" 170 | fi 171 | } 172 | -------------------------------------------------------------------------------- /pkg/src/util/p.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | p.ensure() { 4 | if "$@"; then :; else 5 | util.print_error_die "Command '$*' failed (code $?)" 6 | fi 7 | } 8 | 9 | p.rmln() { 10 | local target="$1" 11 | local link="$2" 12 | 13 | p.ensure rm -rf "$link" 14 | p.ensure ln -sf "$target" "$link" 15 | } 16 | 17 | p.cd() { 18 | local dir="$1" 19 | 20 | p.ensure cd -- "$dir" 21 | } 22 | 23 | p.mkdir() { 24 | local dir="$1" 25 | 26 | p.ensure mkdir -p -- "$dir" 27 | } 28 | 29 | p.fetch() { 30 | local url= 31 | 32 | local arg= 33 | for arg; do case $arg in 34 | -*) 35 | continue 36 | ;; 37 | *) 38 | url=$arg 39 | ;; 40 | esac done; unset -v arg 41 | 42 | if [ "$g_flag_dry_run" = 'yes' ]; then 43 | util.print_info "Would have fetched $url" 44 | return 45 | else 46 | util.print_info "Fetching $url" 47 | fi 48 | 49 | # progress-bar goes to standard error 50 | if [ -t 2 ]; then 51 | # TODO: Alternate screen should have same contents as current screen to prevent jarding 52 | # core.trap_add 'tty.all_restore' INT 53 | # tty.all_save 54 | # util.print_info 'Fetching' "$url" 55 | p.ensure curl -fSL --progress-bar "$@" 56 | # tty.all_restore 57 | # core.trap_remove 'tty.all_restore' INT 58 | else 59 | p.ensure curl -fsSL "$@" 60 | fi 61 | 62 | } 63 | 64 | p.unpack() { 65 | local file= flag_directory= flag_strip='no' 66 | 67 | local arg= 68 | for arg; do case $arg in 69 | -d*) flag_directory=${arg#-d} ;; 70 | -s) flag_strip='yes' ;; 71 | *) file=$arg ;; 72 | esac done; unset -v arg 73 | 74 | local file="$1" 75 | if ! shift; then 76 | util.print_error_die "Failed to shift" 77 | fi 78 | 79 | util.sanitize_path "$PWD/$file" 80 | if [ "$g_flag_dry_run" = 'yes' ]; then 81 | util.print_info "Would have unpacked: $REPLY" 82 | return 83 | else 84 | util.print_info 'Unpacking' "$REPLY" 85 | fi 86 | 87 | if command -v pv &>/dev/null; then 88 | pv "$file" 89 | else 90 | cat "$file" 91 | fi | if [[ $file == *.tar* ]]; then 92 | local -a args=() 93 | if [ -n "$flag_directory" ]; then 94 | args+=('-C' "$flag_directory") 95 | fi 96 | if [ "$flag_strip" = 'yes' ]; then 97 | args+=('--strip-components=1') 98 | fi 99 | 100 | tar xf "$file" "${args[@]}" 101 | elif [[ $file == *.zip ]]; then 102 | local -a args=() 103 | if [ -n "$flag_directory" ]; then 104 | args+=('-d' "$flag_directory") 105 | fi 106 | if [ "$flag_strip" = 'yes' ]; then 107 | core.print_fatal_die "Cannot use strip with zip files" 108 | fi 109 | 110 | p.ensure unzip -qq "$file" "${args[@]}" 111 | else 112 | util.print_error_die "Failed to extract file: '$file'" 113 | fi 114 | } 115 | 116 | p.run_filter() { 117 | local input="$1" 118 | 119 | var.get_dir 'plugins' 120 | local plugins_dir="$REPLY" 121 | 122 | if [[ ${input::1} == '/' ]]; then 123 | p.run_file "$input" "${@:2}" 124 | else 125 | if [ -f "$plugins_dir/woof-plugin-$WOOF_PLUGIN_NAME/tools/filters/$input" ]; then 126 | p.run_file "$plugins_dir/woof-plugin-$WOOF_PLUGIN_NAME/tools/filters/$input" "${@:2}" 127 | else 128 | util.print_error_die "Failed to find filter for argument: '$input'" 129 | fi 130 | fi 131 | } 132 | 133 | p.run_file() { 134 | local file="$1" 135 | 136 | case $file in 137 | *.sh) 138 | BASH_ENV="$BASALT_PACKAGE_DIR/pkg/src/filter_utils/util/util.sh" bash "$file" "${@:2}" 139 | ;; 140 | *.bash) 141 | BASH_ENV="$BASALT_PACKAGE_DIR/pkg/src/filter_utils/util/util.sh" bash "$file" "${@:2}" 142 | ;; 143 | *.jq) 144 | jq -L "$BASALT_PACKAGE_DIR/pkg/src/filter_utils" -rf "$file" "${@:2}" --arg global_default_arch '' 145 | ;; 146 | *.pl) 147 | perl "$file" "${@:2}" 148 | ;; 149 | *) 150 | util.print_error_die "No runner found for file: '$file'" 151 | esac 152 | } 153 | 154 | p.fetch_git_tags() { 155 | local url="$1" 156 | 157 | local {_,refspec}= 158 | while read -r _ refspec; do 159 | printf '%s\n' "${refspec#refs/tags/}" 160 | done < <(git ls-remote --refs --tags "$url") 161 | } 162 | 163 | p.fetch_github_release() { 164 | local repo="$1" 165 | 166 | local -i has_more_pages=2 i=1 167 | for ((i=1; has_more_pages==2; ++i)); do 168 | local url="https://api.github.com/repos/$repo/releases?per_page=100&page=$i" 169 | 170 | # Use 'curl' over p.fetch 171 | if curl -fsSL -H "Authorization: token $GITHUB_TOKEN" "$url" \ 172 | | jq 'if length == 0 then "" else . end | if . == "" then halt_error(29) else . end' 173 | then :; else 174 | local exit_code=$? 175 | 176 | if ((exit_code == 0)); then 177 | continue 178 | elif ((exit_code == 29)); then # '29' is not taken by curl 179 | has_more_pages=0 180 | else 181 | util.print_error_die "Failed to execute curl or jq" 182 | fi 183 | fi 184 | done 185 | } 186 | -------------------------------------------------------------------------------- /pkg/src/util/tty.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | # shellcheck disable=SC2059 4 | tty.fullscreen_init() { 5 | stty -echo 6 | term.cursor_hide -p 7 | term.cursor_savepos -p 8 | term.screen_save -p 9 | 10 | term.erase_saved_lines -p 11 | read -r g_tty_height g_tty_width < <(stty size) 12 | } 13 | 14 | # shellcheck disable=SC2059 15 | tty.fullscreen_deinit() { 16 | term.screen_restore -p 17 | term.cursor_restorepos -p 18 | term.cursor_show -p 19 | stty echo 20 | } 21 | 22 | tty.all_save() { 23 | term.cursor_savepos -p 24 | term.screen_save -p 25 | } 26 | tty.all_restore() { 27 | term.screen_restore -p 28 | term.cursor_restorepos -p 29 | } 30 | 31 | # backwards 32 | tty._backwards_all() { 33 | new_version_index=0 34 | } 35 | 36 | tty._backwards_full_screen() { 37 | if ((new_version_index - g_tty_height > 0)); then 38 | new_version_index=$((new_version_index - g_tty_height)) 39 | else 40 | new_version_index=0 41 | fi 42 | } 43 | 44 | tty._backwards_half_screen() { 45 | if ((new_version_index - (g_tty_height/2) > 0)); then 46 | new_version_index=$((new_version_index - (g_tty_height/2))) 47 | else 48 | new_version_index=0 49 | fi 50 | } 51 | 52 | tty._backwards_one() { 53 | if ((new_version_index > 0)); then 54 | new_version_index=$((new_version_index-1)) 55 | fi 56 | } 57 | 58 | # forwards 59 | tty._forwards_full_screen() { 60 | local array_length=$1 61 | 62 | if ((new_version_index + g_tty_height < array_length)); then 63 | new_version_index=$((new_version_index + g_tty_height)) 64 | else 65 | new_version_index=$((array_length-1)) 66 | fi 67 | } 68 | 69 | tty._forwards_half_screen() { 70 | local array_length=$1 71 | 72 | if ((new_version_index + (g_tty_height/2) < array_length)); then 73 | new_version_index=$((new_version_index + (g_tty_height/2))) 74 | else 75 | new_version_index=$((array_length-1)) 76 | fi 77 | } 78 | 79 | tty._forwards_one() { 80 | local array_length=$1 81 | 82 | if ((new_version_index+1 < array_length)); then 83 | new_version_index=$((new_version_index+1)) 84 | fi 85 | } 86 | 87 | tty._forwards_all() { 88 | local array_length=$1 89 | 90 | new_version_index=$((array_length-1)) 91 | } 92 | 93 | tty._print_list() { 94 | local index="$1" 95 | if ! shift; then 96 | util.print_fatal_die 'Failed to shift' 97 | fi 98 | 99 | # index represents the center (ex. 17) 100 | 101 | local start=$((index - (g_tty_height / 2))) 102 | local end=$((start + g_tty_height)) 103 | 104 | term.cursor_to -p 0 0 105 | 106 | local i= str= prefix= 107 | for ((i=start; i 0 && i<$#+1)); then 120 | str="${prefix}${*:$i:1}" 121 | else 122 | str="${prefix}\033[1;30m~\033[0m" 123 | fi 124 | 125 | printf '\r' 126 | term.erase_line_end -p 127 | # shellcheck disable=SC2059 128 | printf "$str" 129 | done; unset -v i 130 | } 131 | 132 | tty.multiselect() { 133 | unset -v REPLY; REPLY= 134 | local old_version="$1"; if ! shift; then util.print_fatal_die 'Failed to shift'; fi 135 | local select_keys_variable_name="$1"; if ! shift; then util.print_fatal_die 'Failed to shift'; fi 136 | local select_table_variable_name="$1"; if ! shift; then util.print_fatal_die 'Failed to shift'; fi 137 | 138 | local -n select_keys_variable="$select_keys_variable_name" 139 | local -n select_table_variable="$select_table_variable_name" 140 | 141 | if (( ${#select_keys_variable[@]} == 0)); then 142 | util.print_fatal_die "Array should be greater than 0" 143 | fi 144 | 145 | if [ -z "$old_version" ]; then 146 | old_version="${select_keys_variable[0]}" 147 | fi 148 | 149 | # If '$old_version' is not in 'select_table_version', then 150 | if ! [ "${select_table_variable[$old_version]+x}" ]; then 151 | old_version="${select_keys_variable[0]}" 152 | fi 153 | 154 | if ! util.key_to_index "$select_keys_variable_name" "$old_version"; then 155 | tty.fullscreen_deinit 156 | util.print_error_die "Key not '$old_version' not found in array '$select_keys_variable_name'" 157 | fi 158 | old_version_index="$REPLY" 159 | new_version_index="$old_version_index" 160 | 161 | # TODO: # trap 'tty.fullscreen_deinit; exit' EXIT SIGHUP SIGABRT SIGINT SIGQUIT SIGTERM SIGTSTP 162 | trap.sigint_tty() { 163 | tty.fullscreen_deinit 164 | } 165 | core.trap_add 'trap.sigint_tty' 'EXIT' 166 | trap.sigcont_tty() { 167 | tty.fullscreen_init 168 | } 169 | core.trap_add 'trap.sigcont_tty' 'SIGCONT' 170 | 171 | tty.fullscreen_init 172 | 173 | tty._print_list "$new_version_index" "${select_keys_variable[@]}" 174 | while :; do 175 | if ! read -rsN1 key; then 176 | util.print_error_die 'Could not read input' 177 | fi 178 | 179 | case "$key" in 180 | g) tty._backwards_all ;; 181 | $'\x02') tty._backwards_full_screen ;; # C-b 182 | $'\x15') tty._backwards_half_screen ;; # C-u 183 | k|$'\x10') tty._backwards_one ;; # k, C-p 184 | $'\x06') tty._forwards_full_screen ${#select_keys_variable[@]} ;; # C-f 185 | $'\x04') tty._forwards_half_screen ${#select_keys_variable[@]} ;; # C-d 186 | j|$'\x0e') tty._forwards_one ${#select_keys_variable[@]} ;; # j, C-n 187 | G) tty._forwards_all ${#select_keys_variable[@]} ;; 188 | $'\n'|$'\x0d') break ;; # enter (success) 189 | q|$'\x7f') # q, backspace (fail) 190 | new_version_index="$old_version_index" 191 | break 192 | ;; 193 | $'\x1b') # escape 194 | if ! read -rsN1 -t 0.1 key; then 195 | # escape (fail) 196 | new_version_index="$old_version_index" 197 | break 198 | fi 199 | 200 | case "$key" in 201 | $'\x5b') 202 | if ! read -rsN1 -t 0.1 key; then 203 | # escape (fail) 204 | new_version_index="$old_version_index" 205 | break 206 | fi 207 | 208 | case "$key" in 209 | $'\x41') tty._backwards_one ;; # up 210 | $'\x42') tty._forwards_one ${#select_keys_variable[@]} ;; # down 211 | $'\x43') tty._forwards_one ${#select_keys_variable[@]} ;; # right 212 | $'\x44') tty._backwards_one ;; # left 213 | $'\x48') tty._backwards_all ;; # home 214 | $'\x46') tty._forwards_all ${#select_keys_variable[@]} ;; # end 215 | $'\x35') 216 | if ! read -rsN1 -t 0.1 key; then 217 | # escape (fail) 218 | new_version_index="$old_version_index" 219 | break 220 | fi 221 | 222 | case "$key" in 223 | $'\x7e') tty._backwards_full_screen ;; # pageup 224 | esac 225 | ;; 226 | $'\x36') 227 | if ! read -rsN1 -t 0.1 key; then 228 | # escape (fail) 229 | new_version_index="$old_version_index" 230 | break 231 | fi 232 | 233 | case "$key" in 234 | $'\x7e') tty._forwards_full_screen ${#select_keys_variable[@]} ;; # pagedown 235 | esac 236 | esac 237 | ;; 238 | esac 239 | ;; 240 | esac 241 | 242 | tty._print_list "$new_version_index" "${select_keys_variable[@]}" 243 | done 244 | unset -v key 245 | tty.fullscreen_deinit 246 | 247 | core.trap_remove 'trap.sigint_tty' 'EXIT' 248 | core.trap_remove 'trap.sigcont_tty' 'SIGCONT' 249 | 250 | REPLY="${select_keys_variable[$new_version_index]}" 251 | } 252 | -------------------------------------------------------------------------------- /pkg/src/util/util-help.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | util.help_show_part() { 4 | local subcmd="$1" 5 | 6 | case $subcmd in 7 | .init) 8 | printf '\n %s\n' "init [--no-cd] 9 | Print code for a particular shell to set the proper PATH, etc." 10 | ;; 11 | .install) 12 | printf '\n %s\n' "install [--fetch] [--force] [name] [version] 13 | Install a particular tool" 14 | ;; 15 | .uninstall) 16 | printf '\n %s\n' "uninstall [plugin] [version] 17 | Uninstall a particular tool" 18 | ;; 19 | .get-version) 20 | printf '\n %s\n' "get-version [--global] [--full] [plugin] 21 | Get the current version of a tool" 22 | ;; 23 | .set-version) 24 | printf '\n %s\n' "set-version [--global] [plugin] [version] 25 | Set the current version of a tool" 26 | ;; 27 | .exec) 28 | printf '\n %s\n' "exec 29 | Execute the executable of a particular version of a tool" 30 | ;; 31 | .list) 32 | printf '\n %s\n' "list [--global] [--fetch] [--all] [plugin] 33 | List tools" 34 | ;; 35 | .plugin) 36 | printf '\n %s\n' "plugin [...] 37 | Manage a plugin" 38 | ;; 39 | .plugin.install) 40 | printf '\n %s\n' "plugin install [--force] [name...] 41 | Add a plugin" 42 | ;; 43 | .plugin.uninstall) 44 | printf '\n %s\n' "plugin uninstall [name...] 45 | Remove a plugin" 46 | ;; 47 | .plugin.enable) 48 | printf '\n %s\n' "plugin enable [name...] 49 | Enable a plugin" 50 | ;; 51 | .plugin.disable) 52 | printf '\n %s\n' "plugin disable [name...] 53 | Disable a plugin" 54 | ;; 55 | .plugin.info) 56 | printf '\n %s\n' "plugin info [name...] 57 | Show information about a plugin" 58 | ;; 59 | .plugin.list) 60 | printf '\n %s\n' "plugin list 61 | List plugins" 62 | ;; 63 | .tool) 64 | printf '\n %s\n' "tool [...] 65 | Run an internal command" 66 | ;; 67 | .tool.get-exe) 68 | printf '\n %s\n' "tool get-exe 69 | Get the executable of a plugin's tool" 70 | ;; 71 | .tool.print-dirs) 72 | printf '\n %s\n' "tool print-dirs 73 | Print some directories" 74 | ;; 75 | .tool.debug-table) 76 | printf '\n %s\n' "tool debug-table 77 | Run the table() function of a plugin's tool" 78 | ;; 79 | .tool.debug-install) 80 | printf '\n %s\n' "tool debug-install 81 | Run the install() function of a plugin's tool" 82 | ;; 83 | .tool.dev-release) 84 | printf '\n %s\n' "tool dev-release 85 | Release a particular version of the current plugin" 86 | ;; 87 | .tool.clear-table-cache) 88 | printf '\n %s\n' "tool clear-table-cache 89 | Clear the table cache" 90 | ;; 91 | .tool.cd-override) 92 | printf '\n %s\n' "tool cd-override 93 | Override the cd builtin" 94 | ;; 95 | *) 96 | util.print_fatal_die "Unrecognized help command: $1" 97 | esac 98 | } 99 | 100 | # root 101 | util.help_show_cmd_root_all() { 102 | util.help_show_usage_and_flags '' 103 | util.help_show_part '.init' 104 | util.help_show_part '.install' 105 | util.help_show_part '.uninstall' 106 | util.help_show_part '.get-version' 107 | util.help_show_part '.set-version' 108 | util.help_show_part '.exec' 109 | util.help_show_part '.list' 110 | util.help_show_part '.plugin' 111 | util.help_show_part '.tool' 112 | } 113 | 114 | # plugin 115 | util.help_show_cmd_plugin_all() { 116 | util.help_show_usage_and_flags 'plugin ' 117 | util.help_show_part '.plugin.install' 118 | util.help_show_part '.plugin.uninstall' 119 | util.help_show_part '.plugin.enable' 120 | util.help_show_part '.plugin.disable' 121 | util.help_show_part '.plugin.info' 122 | util.help_show_part '.plugin.list' 123 | } 124 | 125 | # tool 126 | util.help_show_cmd_tool_all() { 127 | util.help_show_usage_and_flags 'tool ' 128 | util.help_show_part '.tool.get-exe' 129 | util.help_show_part '.tool.print-dirs' 130 | util.help_show_part '.tool.debug-table' 131 | util.help_show_part '.tool.debug-install' 132 | util.help_show_part '.tool.dev-release' 133 | util.help_show_part '.tool.clear-table-cache' 134 | util.help_show_part '.tool.cd-override' 135 | } 136 | 137 | # "helper" functions 138 | util.help_show_usage_and_flags() { 139 | local subcmd="$1" 140 | subcmd=${subcmd#.} 141 | subcmd=${subcmd//./ } 142 | 143 | printf '%s\n\n' "Usage: 144 | woof [flags] $subcmd [args...]" 145 | 146 | printf '%s' "Flags: 147 | -h, --help 148 | Print help 149 | 150 | -q, --quiet 151 | Do not log informative messages to stdout. Error messages will still be printed 152 | 153 | Subcommand(s):" 154 | } 155 | 156 | -------------------------------------------------------------------------------- /pkg/src/util/util-plugin.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | util.plugin_get_plugins() { 4 | local flag_filter='none' 5 | local flag_with='none' 6 | local arg= 7 | for arg; do case $arg in 8 | --filter=*) 9 | local value=${arg#--filter=} 10 | case $value in 11 | none|active|installed) 12 | flag_with=$value 13 | ;; 14 | *) 15 | util.print_error_die "Flag '$arg' could not be evaluated" 16 | ;; 17 | esac 18 | ;; 19 | --with=*) 20 | local value=${arg#--with=} 21 | case $value in 22 | filepath|name) 23 | flag_with=$value 24 | ;; 25 | *) 26 | util.print_error_die "Flag '$arg' could not be evaluated" 27 | ;; 28 | esac 29 | ;; 30 | *) 31 | util.print_error_die "Flag '$arg' not recognized" 32 | ;; 33 | esac done; unset -v arg 34 | 35 | var.get_dir 'tools' 36 | local tools_dir="$REPLY" 37 | 38 | var.get_dir 'plugins' 39 | local plugins_dir="$REPLY" 40 | 41 | unset -v REPLY 42 | declare -ga REPLY=() 43 | 44 | local dir= plugin_name= entry= 45 | core.shopt_push -s nullglob 46 | for dir in "$plugins_dir/"*/; do 47 | dir=${dir%/} 48 | plugin_name=${dir##*/} 49 | plugin_name=${plugin_name#woof-plugin-} 50 | 51 | if [ "$flag_filter" = 'none' ]; then 52 | : 53 | elif [ "$flag_filter" = 'active' ]; then 54 | if ! util.plugin_is_enabled "$plugin_name"; then 55 | continue 56 | fi 57 | elif [ "$flag_filter" = 'installed' ]; then 58 | if [ ! -d "$tools_dir/$plugin_name" ]; then 59 | continue 60 | fi 61 | fi 62 | 63 | if [ "$flag_with" = 'filepath' ]; then 64 | entry=$dir 65 | elif [ "$flag_with" = 'name' ]; then 66 | entry=${dir##*/} 67 | entry=${entry#woof-plugin-} 68 | fi 69 | 70 | REPLY+=("$entry") 71 | done 72 | core.shopt_pop 73 | unset -v dir plugin_name entry 74 | } 75 | 76 | util.plugin_get_plugin_tools() { 77 | local plugin_name="$1" 78 | 79 | local flag_filter='none' 80 | local flag_with='none' 81 | local arg= 82 | for arg; do case $arg in 83 | --filter=*) 84 | local value=${arg#--filter=} 85 | case $value in 86 | none|active|installed) 87 | flag_with=$value 88 | ;; 89 | *) 90 | util.print_error_die "Flag '$arg' could not be evaluated" 91 | ;; 92 | esac 93 | ;; 94 | --with=*) 95 | local value=${arg#--with=} 96 | case $value in 97 | filepath|name) 98 | flag_with=$value 99 | ;; 100 | *) 101 | util.print_error_die "Flag '$arg' could not be evaluated" 102 | ;; 103 | esac 104 | ;; 105 | -*) 106 | util.print_error_die "Flag '$arg' not recognized" 107 | ;; 108 | esac done; unset -v arg 109 | 110 | var.get_dir 'plugins' 111 | local plugins_dir="$REPLY" 112 | 113 | unset -v REPLY 114 | declare -ga REPLY=() 115 | 116 | local tool= tool_name= entry= 117 | for tool in "$plugins_dir/woof-plugin-$plugin_name/tools/"*.sh; do 118 | if [ "$flag_with" = 'filepath' ]; then 119 | entry=$tool 120 | elif [ "$flag_with" = 'name' ]; then 121 | entry=${tool##*/} 122 | entry=${entry%.sh} 123 | fi 124 | 125 | REPLY+=("$entry") 126 | done 127 | unset -v tool tool_name 128 | } 129 | 130 | util.plugin_get_active_tools_of_plugin() { 131 | local plugin="$1" 132 | 133 | var.get_dir 'plugins' 134 | local plugins_dir="$REPLY" 135 | 136 | unset -v REPLY 137 | declare -ga REPLY=() 138 | 139 | local tool= tool_name= 140 | for tool in "$plugins_dir/woof-plugin-$plugin/tools/"*.sh; do 141 | tool_name=${tool##*/} 142 | tool_name=${tool_name%.sh} 143 | 144 | REPLY+=("$tool_name") 145 | done 146 | unset -v tool tool_name 147 | } 148 | 149 | util.plugin_get_active_tools() { 150 | local flag_with='pair' 151 | local arg= 152 | for arg; do case $arg in 153 | --with=*) 154 | local value=${arg#--with=} 155 | case $value in 156 | pair|toolnameonly|toolfileonly) 157 | flag_with=$value 158 | ;; 159 | *) 160 | util.print_error_die "Flag '$arg' could not be evaluated" 161 | ;; 162 | esac 163 | ;; 164 | *) 165 | util.print_error_die "Flag '$arg' not recognized" 166 | ;; 167 | esac done 168 | 169 | var.get_dir 'plugins' 170 | local plugins_dir="$REPLY" 171 | 172 | unset -v REPLY 173 | declare -ga REPLY=() 174 | 175 | local dir= plugin_name= tool= 176 | for dir in "$plugins_dir/"*/; do 177 | plugin_name=${dir%/}; plugin_name=${plugin_name##*/} 178 | plugin_name=${plugin_name#woof-plugin-} 179 | for tool in "$dir"tools/*.sh; do 180 | if [ "$flag_with" = 'pair' ]; then 181 | tool=${tool##*/}; tool=${tool%.sh} 182 | 183 | REPLY+=("${plugin_name}/${tool}") 184 | elif [ "$flag_with" = 'toolnameonly' ]; then 185 | tool=${tool##*/}; tool=${tool%.sh} 186 | 187 | REPLY+=("$tool") 188 | elif [ "$flag_with" = 'toolfileonly' ]; then 189 | REPLY+=("$tool") 190 | fi 191 | done 192 | done 193 | unset -v dir plugin tool 194 | } 195 | 196 | util.plugin_resolve_external_path() { 197 | var.get_dir 'plugins' 198 | local plugins_dir="$REPLY" 199 | 200 | unset -v REPLY_{SRC,TARGET,TYPE}; REPLY_SRC= REPLY_TARGET= REPLY_TARGET= 201 | local plugin="$1" 202 | 203 | plugin=${plugin%/} 204 | if [ "${plugin::2}" = './' ]; then 205 | REPLY_TYPE='symlink' 206 | REPLY_SRC=$(readlink -f "$plugin") 207 | REPLY_TARGET="$plugins_dir/${plugin##*/}" 208 | elif [ "${plugin::1}" = '/' ]; then 209 | REPLY_TYPE='symlink' 210 | REPLY_SRC=$plugin 211 | REPLY_TARGET="$plugins_dir/${plugin##*/}" 212 | elif [[ "$plugin" == "https://"* ]]; then 213 | REPLY_TYPE='git-repository' 214 | REPLY_SRC=$plugin 215 | REPLY_TARGET="$plugins_dir/${plugin##*/}" 216 | else 217 | util.print_error_die "Passed plugin must be an absolute path, relative path, or https URL" 218 | fi 219 | } 220 | 221 | util.plugin_parse_manifest() { 222 | unset -v REPLY_{SLUG,NAME,DESC,TAGS} 223 | declare -ag REPLY_TAGS=() 224 | 225 | local manifest_file="$1" 226 | util.assert_not_empty 'manifest_file' 227 | 228 | local key= value= 229 | while IFS='=' read -r key value || [[ -n "$key" && -n "$value" ]]; do 230 | key=${key#"${key%%[![:space:]]*}"} 231 | key=${key%"${key##*[![:space:]]}"} 232 | 233 | value=${value#"${value%%[![:space:]]*}"} 234 | value=${value%"${value##*[![:space:]]}"} 235 | if [[ ${value::1} == '"' || ${value::1} == "'" ]]; then 236 | value=${value:1:-1} 237 | fi 238 | 239 | if [ "$key" = 'slug' ]; then 240 | REPLY_SLUG=$value 241 | elif [ "$key" = 'name' ]; then 242 | REPLY_NAME=$value 243 | elif [ "$key" = 'description' ]; then 244 | REPLY_DESC=$value 245 | elif [ "$key" = 'tag' ]; then 246 | REPLY_TAGS+=("$value") 247 | fi 248 | done < "$manifest_file"; unset -v key value 249 | 250 | if [ -z "$REPLY_NAME" ]; then 251 | util.print_error_die "Key 'name' must be set in manifest file: $manifest_file" 252 | fi 253 | } 254 | 255 | 256 | util.plugin_assert_is_valid() { 257 | local plugin_dir="$1" 258 | 259 | if [ ! -d "$plugin_dir" ]; then 260 | util.print_error_die "Plugin could not be found at a non-existent directory: $plugin_dir" 261 | fi 262 | 263 | if [ ! -f "$plugin_dir/manifest.ini" ]; then 264 | util.print_error_die "Plugin manifest does not exist in directory: $plugin_dir" 265 | fi 266 | 267 | # This will fatal if various keys could not be found 268 | util.plugin_parse_manifest "$plugin_dir/manifest.ini" 269 | } 270 | 271 | util.plugin_show_one() { 272 | local plugin_dir="$1" 273 | 274 | util.plugin_assert_is_valid "$plugin_dir" 275 | 276 | util.plugin_parse_manifest "$plugin_dir/manifest.ini" 277 | local name="$REPLY_NAME" desc="$REPLY_DESC" 278 | core.ifs_save 279 | IFS=', '; tags="${REPLY_TAGS[*]}" 280 | core.ifs_restore 281 | 282 | 283 | local plugin_name=${plugin_dir##*/}; 284 | plugin_name=${plugin_name#woof-plugin-} 285 | 286 | term.color_light_blue -Pd "$plugin_name:" 287 | 288 | printf ' ' 289 | term.color_orange -pd 'name:' 290 | term.style_reset -pd 291 | printf ' %s\n' "$name" 292 | 293 | printf ' ' 294 | term.color_orange -pd 'desc:' 295 | term.style_reset -pd 296 | printf ' %s\n' "$desc" 297 | 298 | printf ' ' 299 | term.color_orange -pd 'tags:' 300 | term.style_reset -pd 301 | printf ' %s\n' "${tags:-N/A}" 302 | 303 | local type= 304 | if [ -L "$plugin_dir" ]; then 305 | type='symlink' 306 | else 307 | type='git-repository' 308 | fi 309 | printf ' ' 310 | term.color_orange -pd 'type:' 311 | term.style_reset -pd 312 | printf ' %s\n' "$type" 313 | 314 | local enabled='no' 315 | if util.plugin_is_enabled "$plugin_name"; then 316 | enabled='yes' 317 | fi 318 | printf ' ' 319 | term.color_orange -pd 'enabled:' 320 | term.style_reset -pd 321 | printf ' %s\n' "$enabled" 322 | } 323 | 324 | util.plugin_prune() { 325 | var.get_dir 'plugins' 326 | local plugins_dir="$REPLY" 327 | 328 | core.shopt_push -s nullglob 329 | local file= 330 | for file in "$plugins_dir"/*; do 331 | if [ ! -e "$file" ]; then 332 | unlink "$file" 333 | fi 334 | done 335 | core.shopt_pop 336 | } 337 | 338 | util.plugin_is_enabled() { 339 | local plugin_name="$1" 340 | util.assert_not_empty 'plugin_name' 341 | 342 | var.get_dir 'data' 343 | local dir="$REPLY/disabled_plugins" 344 | util.mkdirp "$dir" 345 | 346 | if [ -f "$dir/$plugin_name" ]; then 347 | return 1 348 | else 349 | return 0 350 | fi 351 | 352 | } 353 | 354 | util.plugin_set_disabled() { 355 | local plugin_name="$1" 356 | util.assert_not_empty 'plugin_name' 357 | 358 | var.get_dir 'data' 359 | local dir="$REPLY/disabled_plugins" 360 | util.mkdirp "$dir" 361 | 362 | touch "$dir/$plugin_name" 363 | } 364 | 365 | util.plugin_set_enabled() { 366 | local plugin_name="$1" 367 | util.assert_not_empty 'plugin_name' 368 | 369 | var.get_dir 'data' 370 | local dir="$REPLY/disabled_plugins" 371 | util.mkdirp "$dir" 372 | 373 | rm -f "$dir/$plugin_name" 374 | } 375 | -------------------------------------------------------------------------------- /pkg/src/util/util-print.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | util.print_fatal_die() { 4 | core.print_fatal "$@" 5 | core.print_stacktrace 6 | exit 1 7 | } 8 | 9 | util.print_error_die() { 10 | core.print_error "$@" 11 | if [ -n "${DEV_MODE+x}" ]; then # TODO: standardize this 12 | core.print_stacktrace 13 | fi 14 | exit 1 15 | } 16 | 17 | util.print_help_die() { 18 | util.help_show_usage_and_flags "$1" 19 | util.help_show_part "$1" 20 | util.print_error_die "${@:2}" 21 | } 22 | 23 | util.print_info() { 24 | if [ "$g_flag_quiet" = 'no' ]; then 25 | core.print_info "$@" 26 | fi 27 | } 28 | 29 | util.print_hint() { 30 | printf '%s\n' " -> $1" 31 | } 32 | -------------------------------------------------------------------------------- /pkg/src/util/util-tool.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | util.get_installed_tools() { 4 | var.get_dir 'tools' 5 | local tools_dir="$REPLY" 6 | 7 | unset -v REPLY; declare -ga REPLY=() 8 | 9 | if [ ! -d "$tools_dir" ]; then 10 | return 11 | fi 12 | 13 | core.shopt_push -s nullglob 14 | local tool_dir= 15 | for tool_dir in "$tools_dir"/*/*/; do 16 | tool_dir=${tool_dir%/} 17 | 18 | plugin_name=${tool_dir%/*}; plugin_name=${plugin_name##*/} 19 | tool_name=${tool_dir##*/} 20 | 21 | printf '%s\n' "$plugin_name/$tool_name" 22 | local tool_version= 23 | for tool_version in "$tool_dir"/*/; do 24 | tool_version=${tool_version%/} 25 | tool_version=${tool_version##*/} 26 | 27 | printf '%s\n' " $tool_version" 28 | done; unset -v version 29 | printf '\n' 30 | done; unset -v tool_dir 31 | core.shopt_pop 32 | } 33 | -------------------------------------------------------------------------------- /pkg/src/util/util-toolversions.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | util.toolversions_get_file() { 4 | unset -v REPLY; REPLY= 5 | 6 | local toolversions_file='.tool-versions' 7 | if ! REPLY=$( 8 | while [ ! -f "$toolversions_file" ] && [ "$PWD" != / ]; do 9 | if ! cd ..; then 10 | exit 1 11 | fi 12 | done 13 | if [ "$PWD" = / ]; then 14 | exit 0 15 | fi 16 | printf '%s' "$PWD/$toolversions_file" 17 | ); then 18 | util.print_error_die "Could not cd when looking for '$toolversions_file'" 19 | fi 20 | } 21 | 22 | util.toolversions_parse() { 23 | local toolversions_path="$1" 24 | local -n __toolversions_variable="$2" 25 | util.assert_not_empty 'toolversions_path' 26 | 27 | local line= 28 | while IFS= read -r line; do 29 | line=${line%%#*} 30 | line=${line#"${line%%[![:space:]]*}"} 31 | line=${line%"${line##*[![:space:]]}"} 32 | 33 | if [ -n "$line" ]; then 34 | local tool_name="${line%% *}" 35 | tool_name=${tool_name%%$'\t'*} 36 | 37 | local tool_versions_str="${line#* }" 38 | tool_versions_str=${tool_versions_str#*$'\t'} 39 | # shellcheck disable=SC2206 40 | tool_versions=($tool_versions_str) 41 | 42 | local old_ifs="$IFS" 43 | IFS='|' 44 | __toolversions_variable[$tool_name]="${tool_versions[*]}" 45 | IFS="$old_ifs"; unset -v old_ifs 46 | fi 47 | 48 | done < "$toolversions_path"; unset -v line 49 | } 50 | -------------------------------------------------------------------------------- /pkg/src/util/util.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | util.get_table_row() { 4 | unset -v REPLY{1,2}; REPLY1= REPLY2= 5 | local tool_name="$1" 6 | local tool_version="$2" 7 | local real_os="$3" 8 | local real_arch="$4" 9 | util.assert_not_empty 'tool_name' 10 | util.assert_not_empty 'tool_version' 11 | util.assert_not_empty 'real_os' 12 | util.assert_not_empty 'real_arch' 13 | 14 | var.get_plugin_table_file "$g_tool_pair" 15 | local table_file="$REPLY" 16 | 17 | if [ ! -f "$table_file" ]; then 18 | util.print_error_die "Expected file '$table_file' to exist" 19 | fi 20 | 21 | if [ -z "$real_os" ] || [ -z "$real_arch" ]; then 22 | util.uname_system 23 | real_os="$REPLY1" 24 | real_arch="$REPLY2" 25 | fi 26 | 27 | local variant= version= os= arch= url= comment= 28 | while IFS='|' read -r variant version os arch url comment; do 29 | if [ "$tool_version" = "$version" ] && [ "$real_os" = "$os" ] && [ "$real_arch" = "$arch" ]; then 30 | REPLY1=$url 31 | REPLY2=$comment 32 | fi 33 | done < "$table_file"; unset -v variant version os arch url comment 34 | 35 | if [ -z "$REPLY1" ]; then 36 | core.print_error "Failed to find a version $tool_version for $tool_name" 37 | util.print_hint "Does the version begin with 'v'? (Example: v18.0.0)" 38 | util.print_hint "Try running 'woof tool clear-version-table $tool_name'" 39 | exit 1 40 | fi 41 | } 42 | 43 | util.run_function() { 44 | local flag_optional='no' 45 | if [ "$1" = '--optional' ]; then # TODO: remove optional? 46 | flag_optional='yes' 47 | if ! shift; then 48 | util.print_error_die 'Failed to shift' 49 | fi 50 | fi 51 | local function_name="$1" 52 | if ! shift; then 53 | util.print_error_die 'Failed to shift' 54 | fi 55 | if declare -f "$function_name" &>/dev/null; then 56 | core.print_debug 'Executing' "$function_name()" 57 | if "$function_name" "$@"; then 58 | return $? 59 | else 60 | return $? 61 | fi 62 | else 63 | if [ "$flag_optional" = 'no' ]; then 64 | util.print_error_die "Function '$function_name' not defined" 65 | fi 66 | fi 67 | } 68 | 69 | util.key_to_index() { 70 | unset -v REPLY; REPLY= 71 | 72 | local -n array_name="$1" 73 | local key="$2" 74 | 75 | local -i index=-1 76 | for ((i=0; i<${#array_name[@]}; ++i)); do 77 | if [ "${array_name[$i]}" = "$key" ]; then 78 | index=$i 79 | break 80 | fi 81 | done; unset -v i 82 | 83 | if ((index == -1)); then 84 | return 1 85 | else 86 | REPLY=$index 87 | fi 88 | } 89 | 90 | util.uname_system() { 91 | unset -v REPLY{1,2}; REPLY1= REPLY2= 92 | local kernel= hardware= 93 | 94 | if ! kernel="$(uname -s)"; then 95 | util.print_error_die "Failed to execute 'uname -s'" 96 | fi 97 | 98 | if ! hardware="$(uname -m)"; then 99 | util.print_error_die "Failed to execute 'uname -m'" 100 | fi 101 | 102 | local kernel_pretty= hardware_pretty= 103 | 104 | # linux|darwin|freebsd 105 | case "$kernel" in 106 | Linux) kernel_pretty='linux' ;; 107 | Darwin) kernel_pretty='darwin' ;; 108 | FreeBSD) kernel_pretty='freebsd' ;; 109 | *) util.print_error_die "Kernel '$kernel' unsupported. Please create a bug report if this is a mistake" ;; 110 | esac 111 | 112 | # x86_64|x86|armv7l|aarch64 113 | case "$hardware" in 114 | i686|x86) hardware_pretty='x86' ;; 115 | amd64|x86_64) hardware_pretty='x86_64' ;; 116 | armv7l) hardware_pretty='armv7l' ;; 117 | aarch64) hardware_pretty='aarch64' ;; 118 | *) util.print_error_die "Hardware '$hardware' unsupported. Please create a bug report if this is a mistake" ;; 119 | esac 120 | 121 | REPLY1="$kernel_pretty" 122 | REPLY2="$hardware_pretty" 123 | } 124 | 125 | util.get_plugin_data() { 126 | unset -v REPLY 127 | declare -g REPLY=() 128 | 129 | local tool_name="$1" 130 | local tool_version="$2" 131 | local specified_key="$3" 132 | util.assert_not_empty 'tool_name' 133 | util.assert_not_empty 'tool_version' 134 | util.assert_not_empty 'specified_key' 135 | 136 | var.get_dir 'tools' "$tool_name" 137 | local install_dir="$REPLY" 138 | 139 | local data_file="$install_dir/$tool_version/.woof_/data.txt" 140 | local key= values= 141 | while IFS='=' read -r key values; do 142 | if [ "$specified_key" = "$key" ]; then 143 | IFS=':' read -ra values <<< "$values" 144 | 145 | REPLY=("${values[@]}") 146 | return 147 | fi 148 | done < "$data_file"; unset -v key values 149 | } 150 | 151 | util.is_tool_version_installed() { 152 | unset -v REPLY; REPLY= 153 | local tool_name="$1" 154 | local tool_version="$2" 155 | util.assert_not_empty 'tool_name' 156 | util.assert_not_empty 'tool_version' 157 | 158 | var.get_dir 'tools' "$tool_name" 159 | local install_dir="$REPLY" 160 | 161 | if [ -f "$install_dir/$tool_version/.woof_/done" ]; then 162 | REPLY="$install_dir/$tool_version" 163 | return 0 164 | else 165 | return 1 166 | fi 167 | } 168 | 169 | util.assert_not_empty() { 170 | local variable_name= 171 | for variable_name; do 172 | local -n __variable="$variable_name" 173 | 174 | if [ -z "$__variable" ]; then 175 | util.print_fatal_die "Failed because variable '$variable_name' is empty" 176 | fi 177 | done; unset -v variable_name 178 | } 179 | 180 | util.sanitize_path() { 181 | unset -v REPLY; REPLY= 182 | local path="$1" 183 | util.assert_not_empty 'path' 184 | 185 | # For now, only do this once (replace '/./' with '/') 186 | path=${path/\/.\//\/} 187 | 188 | local woof_var_name= 189 | for woof_var_name in $WOOF_VARS; do 190 | local -n woof_var="$woof_var_name" 191 | 192 | if [ "${path::${#woof_var}}" = "$woof_var" ]; then 193 | path="\$$woof_var_name${path:${#woof_var}}" 194 | fi 195 | done; unset -v woof_var_name 196 | unset -vn woof_var 197 | 198 | REPLY="$path" 199 | } 200 | 201 | util.sort_versions() { 202 | sort -V 203 | } 204 | 205 | util.mkdirp() { 206 | local dir="$1" 207 | 208 | if [ ! -d "$dir" ]; then 209 | mkdir -p "$dir" 210 | fi 211 | } 212 | 213 | # TODO 214 | util.path_things() { 215 | # Remove all Woof-related PATH entries 216 | local new_path= 217 | local entries= 218 | IFS=':' read -ra entries <<< "$PATH" 219 | local entry= 220 | for entry in "${entries[@]}"; do 221 | if [[ $entry != *"$WOOF_STATE_HOME"* ]]; then 222 | new_path="${new_path:+"$new_path:"}$entry" 223 | fi 224 | done 225 | 226 | printf '%s\n' '# --- plugins ----' 227 | util.plugin_get_active_tools --with=toolfileonly 228 | local tool_file= tool_name= 229 | for tool_file in "${REPLY[@]}"; do 230 | # shellcheck disable=SC1090 231 | source "$tool_file" 232 | tool_name=${tool_file##*/}; tool_name=${tool_name%*.sh} 233 | 234 | if command -v "$tool_name".env &>/dev/null; then 235 | printf '%s\n' "# $tool_name" 236 | "$tool_name".env 237 | printf '\n' 238 | fi 239 | 240 | util.tool_get_global_version --no-error "$tool_name" 241 | local tool_version="$REPLY" 242 | if [ -n "$tool_version" ]; then 243 | util.get_plugin_data "$tool_name" "$tool_version" 'bins' 244 | local -a bins=("${REPLY[@]}") 245 | local bin= 246 | for bin in "${bins[@]}"; do 247 | bin=${bin#./} 248 | 249 | var.get_dir 'tools' "$tool_name" 250 | local install_dir="$REPLY" 251 | local bin_dir="$install_dir/$tool_version/$bin" 252 | 253 | new_path="$bin_dir${new_path:+":$new_path"}" 254 | done; unset -v bin 255 | fi 256 | done; unset -v tool_file tool_name 257 | 258 | # Get each currently active global version (for now only global) TODO 259 | for file in "$WOOF_STATE_HOME/data/selection"/*/*; do 260 | declare -g g_plugin_name=${file%/*}; g_plugin_name=${g_plugin_name##*/} 261 | declare -g g_tool_name=${file##*/} 262 | declare -g g_tool_version= 263 | g_tool_version=$(<"$file") 264 | declare -g g_tool_pair=$g_plugin_name/$g_tool_name 265 | 266 | # local install_dir="$WOOF_STATE_HOME/tools/$g_tool_name/$g_tool_name/$g_tool_version" 267 | var.get_dir 'tools' "$g_tool_pair" 268 | local install_dir="$REPLY/$g_tool_version" 269 | 270 | util.get_plugin_data "$g_tool_pair" "$g_tool_version" 'bins' 271 | local bin_dir= 272 | for bin_dir in "${REPLY[@]}"; do 273 | bin_dir=${bin_dir#./} 274 | local tool_dir="$install_dir/$bin_dir" 275 | 276 | new_path="$tool_dir${new_path:+":$new_path"}" 277 | done; unset -v bin_dir 278 | done 279 | 280 | printf '%s\n' '# --- path ----' 281 | printf '%s\n' "PATH=$new_path" 282 | } 283 | 284 | util.get_latest_tool_version() { 285 | unset -v REPLY; REPLY= 286 | local tool_pair="$1" 287 | local real_os="$2" 288 | local real_arch="$3" 289 | util.assert_not_empty 'tool_pair' 290 | util.assert_not_empty 'real_os' 291 | util.assert_not_empty 'real_arch' 292 | 293 | var.get_plugin_table_file "$g_tool_pair" 294 | local table_file="$REPLY" 295 | 296 | local variant= version= os= arch= url= comment= 297 | while IFS='|' read -r variant version os arch url comment; do 298 | if [ "$real_os" = "$os" ] && [ "$real_arch" = "$arch" ]; then 299 | REPLY="$version" 300 | break 301 | fi 302 | done < "$table_file"; unset -v version os arch url comment 303 | } 304 | 305 | util.determine_tool_pair() { 306 | local input="$1" 307 | local filter="$2" 308 | 309 | local tool_pair= 310 | local plugin_name= 311 | local tool_name= 312 | 313 | if [ -z "$input" ]; then 314 | util.plugin_get_plugins --filter="$filter" --with=name 315 | local -a all_plugins_arr=("${REPLY[@]}") 316 | local -A all_plugins_obj=() 317 | for m in "${all_plugins_arr[@]}"; do 318 | all_plugins_obj["$m"]= 319 | done; unset -v m 320 | tty.multiselect '' all_plugins_arr all_plugins_obj 321 | plugin_name="$REPLY" 322 | 323 | util.plugin_get_plugin_tools "$plugin_name" --filter="$filter" 324 | util.plugin_get_active_tools_of_plugin "$plugin_name" 325 | local all_tools_arr=("${REPLY[@]}") 326 | local -A all_tools_obj=() 327 | for m in "${all_tools_arr[@]}"; do 328 | all_tools_obj["$m"]= 329 | done; unset -v m 330 | tty.multiselect '' all_tools_arr all_tools_obj 331 | tool_name="$REPLY" 332 | 333 | tool_pair="$plugin_name/$tool_name" 334 | elif [[ "$input" != */* ]]; then 335 | # Input might be a tool, search for it in currently enabled plugins 336 | util.plugin_get_active_tools --with=pair 337 | local tools=("${REPLY[@]}") 338 | local tool= 339 | for tool in "${tools[@]}"; do 340 | if [ "$input" = "${tool#*/}" ]; then 341 | plugin_name=${tool%/*} 342 | tool_name=${tool#*/} 343 | 344 | tool_pair="$plugin_name/$tool_name" 345 | break 346 | fi 347 | done 348 | 349 | # Input might be a plugin, earch for currently enabled plugins 350 | if [ -z "$tool_pair" ]; then 351 | var.get_dir 'plugins' 352 | local plugins_dir="$REPLY" 353 | if [ -d "$plugins_dir/woof-plugin-$input" ]; then 354 | util.plugin_get_active_tools_of_plugin "$input" 355 | local all_tools_arr=("${REPLY[@]}") 356 | local -A all_tools_obj=() 357 | for m in "${all_tools_arr[@]}"; do 358 | all_tools_obj["$m"]= 359 | done; unset -v m 360 | tty.multiselect '' all_tools_arr all_tools_obj 361 | local _tool_name="$REPLY" 362 | 363 | plugin_name=$input 364 | tool_name="$_tool_name" 365 | 366 | tool_pair="$plugin_name/$tool_name" 367 | fi 368 | fi 369 | 370 | if [ -z "$tool_pair" ]; then 371 | util.print_error_die "Failed to find tool or plugin with name: $input" 372 | fi 373 | else 374 | plugin_name=${input%/*} 375 | tool_name=${input#*/} 376 | 377 | tool_pair=$input 378 | fi 379 | 380 | REPLY1=$tool_pair 381 | REPLY2=$plugin_name 382 | REPLY3=$tool_name 383 | } 384 | -------------------------------------------------------------------------------- /pkg/src/util/var.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | var.get_tool_file() { 4 | var.get_dir 'plugins' 5 | local plugins_dir="$REPLY" 6 | 7 | unset -v REPLY 8 | REPLY="$plugins_dir/woof-plugin-$1/tools/$2.sh" 9 | } 10 | 11 | var.get_plugin_table_file() { 12 | unset -v REPLY 13 | REPLY="$WOOF_CACHE_HOME/tables/$1.txt" 14 | } 15 | 16 | var.get_plugin_workspace_dir() { 17 | unset -v REPLY 18 | REPLY="$WOOF_STATE_HOME/workspace-$1" 19 | } 20 | 21 | var.get_dir() { 22 | unset -v REPLY 23 | REPLY="$WOOF_STATE_HOME/$1${2:+/$2}" 24 | } 25 | -------------------------------------------------------------------------------- /tests/helper_toolversions_parse.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load './util/init.sh' 4 | 5 | @test "Works in base case 1" { 6 | cat > '.tool-versions' <<-"EOF" 7 | ruby 2.5.3 8 | EOF 9 | 10 | declare -gA tools=() 11 | util.toolversions_parse '.tool-versions' 'tools' 12 | assert [ "${#tools[@]}" -eq '1' ] 13 | test_index_object_keys 'tools' '0' 14 | assert [ "$REPLY" = 'ruby' ] 15 | assert [ "${tools[ruby]}" = '2.5.3' ] 16 | } 17 | 18 | @test "Works in base case 2" { 19 | cat > '.tool-versions' <<-"EOF" 20 | ruby 2.5.3 21 | EOF 22 | 23 | declare -gA tools=() 24 | util.toolversions_parse '.tool-versions' 'tools' 25 | assert [ "${#tools[@]}" -eq '1' ] 26 | test_index_object_keys 'tools' '0' 27 | assert [ "$REPLY" = 'ruby' ] 28 | assert [ "${tools[ruby]}" = '2.5.3' ] 29 | } 30 | 31 | @test "Works with multiple versions" { 32 | cat > '.tool-versions' <<-"EOF" 33 | nodejs 18.0.0 17.0.1 17.0.0 system 34 | EOF 35 | 36 | declare -gA tools=() 37 | util.toolversions_parse '.tool-versions' 'tools' 38 | assert [ "${#tools[@]}" -eq '1' ] 39 | test_index_object_keys 'tools' '0' 40 | assert [ "$REPLY" = 'nodejs' ] 41 | assert [ "${tools[nodejs]}" = '18.0.0|17.0.1|17.0.0|system' ] 42 | } 43 | 44 | @test "Works with comments" { 45 | cat > '.tool-versions' <<-"EOF" 46 | nodejs 18.0.0 system# uwu 47 | # Gamma 48 | ruby 2.7.0 49 | EOF 50 | 51 | declare -gA tools=() 52 | util.toolversions_parse '.tool-versions' 'tools' 53 | assert [ "${#tools[@]}" -eq '2' ] 54 | test_index_object_keys 'tools' '0' 55 | assert [ "$REPLY" = 'nodejs' ] 56 | assert [ "${tools[nodejs]}" = '18.0.0|system' ] 57 | test_index_object_keys 'tools' '1' 58 | assert [ "$REPLY" = 'ruby' ] 59 | assert [ "${tools[ruby]}" = '2.7.0' ] 60 | } 61 | 62 | @test "Works with tabs" { 63 | cat > '.tool-versions' <<-"EOF" 64 | ruby 2.5.3 2.8.3 65 | EOF 66 | 67 | declare -gA tools=() 68 | util.toolversions_parse '.tool-versions' 'tools' 69 | assert [ "${#tools[@]}" -eq '1' ] 70 | test_index_object_keys 'tools' '0' 71 | assert [ "$REPLY" = 'ruby' ] 72 | assert [ "${tools[ruby]}" = '2.5.3|2.8.3' ] 73 | } 74 | -------------------------------------------------------------------------------- /tests/install.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load './util/init.sh' 4 | 5 | setup() { 6 | eval "$("$BASALT_PACKAGE_DIR/pkg/bin/woof" init bash)" 7 | export WOOF_INTERNAL_TESTING='yes' 8 | export WOOF_CONFIG_HOME="$BATS_TEST_TMPDIR/.config" 9 | export WOOF_CACHE_HOME="$BATS_TEST_TMPDIR/.cache" 10 | export WOOF_DATA_HOME="$BATS_TEST_TMPDIR/.local/share" 11 | export WOOF_STATE_HOME="$BATS_TEST_TMPDIR/.local/state" 12 | cd "$BATS_TEST_TMPDIR" 13 | } 14 | 15 | @test "Installing with full plugin path" { 16 | woof plugin install "$BATS_TEST_DIRNAME/stubs/woof-plugin-basictest" 17 | woof install basictest/tool1 v1 18 | 19 | run woof plugin list 20 | [[ "$output" == *basictest* ]] 21 | } 22 | -------------------------------------------------------------------------------- /tests/stubs/woof-plugin-basictest/manifest.ini: -------------------------------------------------------------------------------- 1 | name = basictest 2 | description = Stub used for testing basic functionality of plugins 3 | version = 0.1.0 4 | -------------------------------------------------------------------------------- /tests/stubs/woof-plugin-basictest/tools/tool1.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | tool1.table() { 4 | cat <<"EOF" 5 | T1|v1|linux|x86_64|https://example.com/downloads/tool1-v1-linux-x86_64.tar.gz 6 | T1|v2|linux|x86_64|https://example.com/downloads/tool1-v2-linux-x86_64.tar.gz 7 | EOF 8 | } 9 | 10 | tool1.install() { 11 | local url="$1" 12 | local version="$2" 13 | 14 | mkdir -p './dir/bin' 15 | if [ "$version" = 'v1' ]; then 16 | printf '%s\n' 'echo "hello v1"' >> './dir/bin/tool1' 17 | elif [ "$version" = 'v2' ]; then 18 | printf '%s\n' '"echo hello v2"' >> './dir/bin/tool1' 19 | fi 20 | chmod +x './dir/bin/tool1' 21 | 22 | REPLY_DIR='./dir' 23 | REPLY_BINS=('./bin') 24 | } 25 | -------------------------------------------------------------------------------- /tests/toolversions_cd.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load './util/init.sh' 4 | 5 | setup() { 6 | eval "$("$BASALT_PACKAGE_DIR/pkg/bin/woof" init bash)" 7 | cd "$BATS_TEST_TMPDIR" 8 | } 9 | 10 | @test "Warns if using ref protocol" { 11 | cat > '.tool-versions' <<-"EOF" 12 | ruby ref:v0.1 13 | EOF 14 | 15 | run cd . 16 | assert [ "$status" -eq 0 ] 17 | assert_line -p "as 'ref:' is not yet supported" 18 | } 19 | 20 | @test "Warns if using path protocol" { 21 | cat > '.tool-versions' <<-"EOF" 22 | ruby path:../other 23 | EOF 24 | 25 | run cd . 26 | assert [ "$status" -eq 0 ] 27 | assert_line -p "as 'path:' is not yet supported" 28 | } 29 | 30 | @test "Warns if using system" { 31 | cat > '.tool-versions' <<-"EOF" 32 | ruby system 33 | EOF 34 | 35 | run cd . 36 | assert [ "$status" -eq 0 ] 37 | assert_line -p "as 'system' is not yet supported" 38 | } 39 | -------------------------------------------------------------------------------- /tests/util/init.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | eval "$(basalt-package-init)" || exit 4 | basalt.package-init || exit 5 | basalt.package-load 6 | basalt.load 'github.com/hyperupcall/bats-all' 'load.bash' 7 | 8 | load './util/test_utils.sh' 9 | 10 | setup() { 11 | cd "$BATS_TEST_TMPDIR" 12 | } 13 | 14 | teardown() { 15 | cd "$BATS_SUITE_TMPDIR" 16 | } 17 | -------------------------------------------------------------------------------- /tests/util/test_utils.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | test_index_object_keys() { 4 | unset -v REPLY; REPLY= 5 | local object_name="$1" 6 | local n="$2" 7 | 8 | local -n __object="$object_name" 9 | 10 | local key= i=0 11 | for key in "${!__object[@]}"; do 12 | if ((i == n)); then 13 | REPLY="$key" 14 | fi 15 | 16 | i=$((i+1)) 17 | done 18 | } 19 | 20 | test_snapshot_cmd() { 21 | local filename="$1" 22 | if ! shift; then 23 | core.fatal 'Failed to shift' 24 | fi 25 | 26 | local snapshot_file="$BATS_TEST_DIRNAME/snapshots/$filename" 27 | 28 | util.mkdirp "${snapshot_file%/*}" 29 | 30 | "$@" > "$snapshot_file" 31 | } 32 | --------------------------------------------------------------------------------