├── .editorconfig ├── .gitattributes ├── .gitignore ├── .travis.yml ├── Baskfile.sh ├── LICENSE.md ├── README.md ├── bin └── bask ├── completion └── bask.bash ├── package.json ├── src ├── bask.sh └── cli.sh └── test ├── Baskfile.sh └── test.sh /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.json] 13 | indent_size = 2 14 | 15 | [*.md] 16 | max_line_length = 80 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.sh text eol=lf 3 | *.md text eol=lf 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # .gitignore 2 | 3 | ## ------------------------------------ 4 | ## Project build rubbish 5 | ## ------------------------------------ 6 | 7 | /node_modules 8 | /deps 9 | /.tmp 10 | /*.tgz 11 | npm-debug.log 12 | 13 | 14 | ## ------------------------------------ 15 | ## Normally ignored rubbish 16 | ## ------------------------------------ 17 | 18 | ## temp files 19 | *~ 20 | *.tmp 21 | *.swp 22 | *.swo 23 | .swp.* 24 | 25 | ## OS generated files 26 | __MACOSX 27 | .DS_Store 28 | .Spotlight* 29 | .Trash* 30 | ehthumbs.db 31 | Thumbs.db 32 | 33 | ## ctags 34 | tags 35 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: bash 2 | 3 | notifications: 4 | email: false 5 | 6 | # From the shellcheck Wiki on Travis 7 | addons: 8 | apt: 9 | sources: 10 | - debian-sid 11 | packages: 12 | - shellcheck 13 | 14 | script: 15 | - ./bin/bask 16 | -------------------------------------------------------------------------------- /Baskfile.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | shopt -s globstar 4 | 5 | get_shell_files() { 6 | echo "Finding shell files..." 1>&2 7 | 8 | for file in ./bin/**/* ./src/**/*; do 9 | # check for .sh ending 10 | if [[ "$file" =~ .sh$ ]]; then 11 | # Also report to stderr so we can get a log 12 | echo "$file" | tee /dev/fd/2 13 | # check if has bash in shebang on first line, and print if so 14 | elif [ -f "$file" ]; then 15 | # Also report to stderr so we can get a log 16 | awk '/#!\/.*bash/ && NR < 2 { print FILENAME; }' "$file" | tee /dev/fd/2 17 | fi 18 | done 19 | 20 | echo "Done finding shell files." 1>&2 21 | } 22 | 23 | get_files_and_check() { 24 | get_shell_files | xargs shellcheck 25 | } 26 | 27 | task_default() { 28 | get_files_and_check 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | GNU Lesser General Public License 2 | ================================= 3 | 4 | _Version 3, 29 June 2007_ 5 | _Copyright © 2007 Free Software Foundation, Inc. <>_ 6 | 7 | Everyone is permitted to copy and distribute verbatim copies 8 | of this license document, but changing it is not allowed. 9 | 10 | 11 | This version of the GNU Lesser General Public License incorporates 12 | the terms and conditions of version 3 of the GNU General Public 13 | License, supplemented by the additional permissions listed below. 14 | 15 | ### 0. Additional Definitions 16 | 17 | As used herein, “this License” refers to version 3 of the GNU Lesser 18 | General Public License, and the “GNU GPL” refers to version 3 of the GNU 19 | General Public License. 20 | 21 | “The Library” refers to a covered work governed by this License, 22 | other than an Application or a Combined Work as defined below. 23 | 24 | An “Application” is any work that makes use of an interface provided 25 | by the Library, but which is not otherwise based on the Library. 26 | Defining a subclass of a class defined by the Library is deemed a mode 27 | of using an interface provided by the Library. 28 | 29 | A “Combined Work” is a work produced by combining or linking an 30 | Application with the Library. The particular version of the Library 31 | with which the Combined Work was made is also called the “Linked 32 | Version”. 33 | 34 | The “Minimal Corresponding Source” for a Combined Work means the 35 | Corresponding Source for the Combined Work, excluding any source code 36 | for portions of the Combined Work that, considered in isolation, are 37 | based on the Application, and not on the Linked Version. 38 | 39 | The “Corresponding Application Code” for a Combined Work means the 40 | object code and/or source code for the Application, including any data 41 | and utility programs needed for reproducing the Combined Work from the 42 | Application, but excluding the System Libraries of the Combined Work. 43 | 44 | ### 1. Exception to Section 3 of the GNU GPL 45 | 46 | You may convey a covered work under sections 3 and 4 of this License 47 | without being bound by section 3 of the GNU GPL. 48 | 49 | ### 2. Conveying Modified Versions 50 | 51 | If you modify a copy of the Library, and, in your modifications, a 52 | facility refers to a function or data to be supplied by an Application 53 | that uses the facility (other than as an argument passed when the 54 | facility is invoked), then you may convey a copy of the modified 55 | version: 56 | 57 | * **a)** under this License, provided that you make a good faith effort to 58 | ensure that, in the event an Application does not supply the 59 | function or data, the facility still operates, and performs 60 | whatever part of its purpose remains meaningful, or 61 | 62 | * **b)** under the GNU GPL, with none of the additional permissions of 63 | this License applicable to that copy. 64 | 65 | ### 3. Object Code Incorporating Material from Library Header Files 66 | 67 | The object code form of an Application may incorporate material from 68 | a header file that is part of the Library. You may convey such object 69 | code under terms of your choice, provided that, if the incorporated 70 | material is not limited to numerical parameters, data structure 71 | layouts and accessors, or small macros, inline functions and templates 72 | (ten or fewer lines in length), you do both of the following: 73 | 74 | * **a)** Give prominent notice with each copy of the object code that the 75 | Library is used in it and that the Library and its use are 76 | covered by this License. 77 | * **b)** Accompany the object code with a copy of the GNU GPL and this license 78 | document. 79 | 80 | ### 4. Combined Works 81 | 82 | You may convey a Combined Work under terms of your choice that, 83 | taken together, effectively do not restrict modification of the 84 | portions of the Library contained in the Combined Work and reverse 85 | engineering for debugging such modifications, if you also do each of 86 | the following: 87 | 88 | * **a)** Give prominent notice with each copy of the Combined Work that 89 | the Library is used in it and that the Library and its use are 90 | covered by this License. 91 | 92 | * **b)** Accompany the Combined Work with a copy of the GNU GPL and this license 93 | document. 94 | 95 | * **c)** For a Combined Work that displays copyright notices during 96 | execution, include the copyright notice for the Library among 97 | these notices, as well as a reference directing the user to the 98 | copies of the GNU GPL and this license document. 99 | 100 | * **d)** Do one of the following: 101 | - **0)** Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | - **1)** Use a suitable shared library mechanism for linking with the 109 | Library. A suitable mechanism is one that **(a)** uses at run time 110 | a copy of the Library already present on the user's computer 111 | system, and **(b)** will operate properly with a modified version 112 | of the Library that is interface-compatible with the Linked 113 | Version. 114 | 115 | * **e)** Provide Installation Information, but only if you would otherwise 116 | be required to provide such information under section 6 of the 117 | GNU GPL, and only to the extent that such information is 118 | necessary to install and execute a modified version of the 119 | Combined Work produced by recombining or relinking the 120 | Application with a modified version of the Linked Version. (If 121 | you use option **4d0**, the Installation Information must accompany 122 | the Minimal Corresponding Source and Corresponding Application 123 | Code. If you use option **4d1**, you must provide the Installation 124 | Information in the manner specified by section 6 of the GNU GPL 125 | for conveying Corresponding Source.) 126 | 127 | ### 5. Combined Libraries 128 | 129 | You may place library facilities that are a work based on the 130 | Library side by side in a single library together with other library 131 | facilities that are not Applications and are not covered by this 132 | License, and convey such a combined library under terms of your 133 | choice, if you do both of the following: 134 | 135 | * **a)** Accompany the combined library with a copy of the same work based 136 | on the Library, uncombined with any other library facilities, 137 | conveyed under the terms of this License. 138 | * **b)** Give prominent notice with the combined library that part of it 139 | is a work based on the Library, and explaining where to find the 140 | accompanying uncombined form of the same work. 141 | 142 | ### 6. Revised Versions of the GNU Lesser General Public License 143 | 144 | The Free Software Foundation may publish revised and/or new versions 145 | of the GNU Lesser General Public License from time to time. Such new 146 | versions will be similar in spirit to the present version, but may 147 | differ in detail to address new problems or concerns. 148 | 149 | Each version is given a distinguishing version number. If the 150 | Library as you received it specifies that a certain numbered version 151 | of the GNU Lesser General Public License “or any later version” 152 | applies to it, you have the option of following the terms and 153 | conditions either of that published version or of any later version 154 | published by the Free Software Foundation. If the Library as you 155 | received it does not specify a version number of the GNU Lesser 156 | General Public License, you may choose any version of the GNU Lesser 157 | General Public License ever published by the Free Software Foundation. 158 | 159 | If the Library as you received it specifies that a proxy can decide 160 | whether future versions of the GNU Lesser General Public License shall 161 | apply, that proxy's public statement of acceptance of any version is 162 | permanent authorization for you to choose that version for the 163 | Library. 164 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bask 2 | 3 | [![Travis](https://img.shields.io/travis/jez/bask.svg?maxAge=2592000)](https://travis-ci.org/jez/bask) 4 | [![npm](https://img.shields.io/npm/v/bask.svg?maxAge=2592000)](https://www.npmjs.com/package/bask) 5 | [![Gitter](https://img.shields.io/gitter/room/jez/bask.svg?maxAge=2592000)][gitter] 6 | [![standard-readme compliant](https://img.shields.io/badge/standard--readme-OK-brightgreen.svg)](https://github.com/RichardLitt/standard-readme) 7 | [![tl;drLegal LGPL 3.0](https://img.shields.io/badge/tl%3BdrLegal-LGPL_3.0-blue.svg)](https://tldrlegal.com/license/gnu-lesser-general-public-license-v3-(lgpl-3)) 8 | 9 | > :sunglasses: Bask in the convenience of a task runner for bash 10 | 11 | Bask is a task runner for Bash. It's like Make with a bunch of shell targets, 12 | but without the `.PHONY`'s everywhere, and nicer because there's no more Make 13 | syntax. The main difference is that tasks are always run in Bask, but only run 14 | if targets are out of date in Make. 15 | 16 | If you're writing a Makefile with all `.PHONY` targets, chances are that Bask is 17 | really what you want. 18 | 19 | 20 | 21 | ## Table of Contents 22 | 23 | - [Background](#background) 24 | - [Install](#install) 25 | - [Simple (vendored)](#simple-vendored) 26 | - [Submodule (vendored)](#submodule-vendored) 27 | - [Homebrew](#homebrew) 28 | - [NPM](#npm) 29 | - [Git + PATH](#git--path) 30 | - [Usage](#usage) 31 | - [CLI](#cli) 32 | - [Flags](#flags) 33 | - [Tab Completion](#tab-completion) 34 | - [API](#api) 35 | - [Baskfile](#baskfile) 36 | - [Default Tasks](#default-tasks) 37 | - [Methods](#methods) 38 | - [`bask_depends`](#bask_depends) 39 | - [`bask_fork_join`](#bask_fork_join) 40 | - [`bask_run`](#bask_run) 41 | - [`bask_log`](#bask_log) 42 | - [`bask_colorize`](#bask_colorize) 43 | - [`bask_list_tasks`](#bask_list_tasks) 44 | - [`bask_is_task_defined`](#bask_is_task_defined) 45 | - [Contribute](#contribute) 46 | - [License](#license) 47 | 48 | 49 | 50 | ## Background 51 | 52 | Bask was initially forked from [bash-task-runner]. It was forked for a couple 53 | reasons: 54 | 55 | - I needed to vendor it for an unrelated project. 56 | - I wanted to drop the dependency on GNU coreutils. 57 | - I wanted to improve the name :smirk: 58 | 59 | I actively follow upstream changes. If I'm missing a compatible feature that was 60 | added upstream, feel free to notify me via an issue, pull request, or message on 61 | Gitter. 62 | 63 | Bask's first commit is a squashed version of `bash-task-runner` at the time it 64 | was forked. For a complete list of changes, just use the commit log. 65 | 66 | 67 | ## Install 68 | 69 | Each of the below installation methods is differentiated along two properties: 70 | 71 | - **Local to project** 72 | - Whether Bask will be installed locally to your project or globally on a 73 | system. 74 | - This is good for CI builds and spinning up multiple people on your project 75 | - **CLI-enabled** 76 | - Whether you will be able to use the `bask` command from your prompt. 77 | - Useful for local development, tab completion, and convenience. 78 | 79 | You may want to combine multiple installation methods in order to satisfy both 80 | of these requirements. In particular, we recommend [**Simple 81 | (vendored)**](#simple-vendored) with a method that gives you a CLI and is 82 | compatible with your system. 83 | 84 | | | Local to Project | CLI-enabled | 85 | | --- | --- | --- | 86 | | Simple (vendored) | :white_check_mark: | :no_entry_sign: | 87 | | Submodule (vendored) | :white_check_mark: | :white_check_mark: | 88 | | Homebrew | :no_entry_sign: | :white_check_mark: | 89 | | NPM | :white_check_mark: | :white_check_mark: | 90 | | Git + PATH | :no_entry_sign: | :white_check_mark: | 91 | 92 | ### Dependencies 93 | 94 | Currently Bask requires Bash 4.2+. 95 | 96 | ### Simple (vendored) 97 | 98 | Just drop `src/bask.sh` anywhere in your project folder: 99 | 100 | ```shell 101 | wget https://raw.githubusercontent.com/jez/bask/master/src/bask.sh 102 | ``` 103 | 104 | Then skip to [Usage](#usage) for how to use a vendored Bask installation. 105 | 106 | ### Submodule (vendored) 107 | 108 | If you'd like a slightly better story around updating Bask when vendored, you 109 | can use a Git submodule, if you're [familiar with submodules][submodules]: 110 | 111 | ```shell 112 | git submodule add https://github.com/jez/bask 113 | ``` 114 | 115 | > Note that if submodules are too heavy-handed, you can get the same effect 116 | > (without the ease of updating) by just unzip'ing Bask's source into your 117 | > project. 118 | 119 | You should now be able to access `bask.sh` within the submodule. Additionally, 120 | you can access the CLI with `./bask/bin/bask`. You can make this more ergonomic 121 | by altering your PATH: 122 | 123 | ```shell 124 | export PATH="$PATH:./bask/bin" 125 | ``` 126 | 127 | Then skip to [Usage](#usage) to learn more. 128 | 129 | ### Homebrew 130 | 131 | On OS X, installing Bask globally is simple if you have Homebrew: 132 | 133 | ```shell 134 | brew install jez/formulae/bask 135 | ``` 136 | 137 | Then skip to [Usage](#usage) to learn more. 138 | 139 | ### NPM 140 | 141 | If you don't mind the additional dependency on the NPM ecosystem, you can 142 | install Bask with NPM: 143 | 144 | ```shell 145 | # --- Local to Project --- # 146 | npm install --save bask 147 | 148 | # to enable CLI: 149 | export PATH="PATH:./node_modules/.bin" 150 | 151 | # --- Global --- # 152 | npm install -g bask 153 | ``` 154 | 155 | Then skip to [Usage](#usage) to learn more. 156 | 157 | ### Git + PATH 158 | 159 | If Bask is not available in a package manager for your system, you can clone 160 | Bask to your computer, and adjust your PATH to contain the installation 161 | location: 162 | 163 | ``` 164 | git clone https://github.com/jez/bask 165 | 166 | export PATH="$PATH:$(pwd)/bask/bin" 167 | ``` 168 | 169 | Then skip to [Usage](#usage) for how to use the CLI. 170 | 171 | 172 | ## Usage 173 | 174 | You use Bask in conjunction with a `Baskfile`. A basic `Baskfile` looks like 175 | this: 176 | 177 | ```shell 178 | task_foo() { 179 | ## Do something... 180 | } 181 | 182 | task_bar() { 183 | ## Do something... 184 | } 185 | ``` 186 | 187 | **Optional**: if you want your `Baskfile` to be a standalone script, add this 188 | to the beginning (works best in conjunction with a vendored installation): 189 | 190 | ```shell 191 | #!/usr/bin/env bash 192 | cd "$(dirname "$0")" || exit 193 | source ./path/to/bask.sh 194 | ``` 195 | 196 | You invoke Bask using `bask [task ...]`: 197 | 198 | ```console 199 | $ bask foo bar 200 | [21:37:43.754] Starting 'foo' 201 | [21:37:43.755] Finished 'foo' after 1 ms 202 | [21:37:43.756] Starting 'bar' 203 | [21:37:43.757] Finished 'bar' after 1 ms 204 | ``` 205 | 206 | Or, if your `Baskfile` sources `bash.sh`: 207 | 208 | ```console 209 | bash Baskfile foo bar 210 | ``` 211 | 212 | ## CLI 213 | 214 | NOTE: Please see `bask -h` for complete, up-to-date CLI usage information. 215 | 216 | ``` 217 | Usage: bask [options] [task] [task_options] ... 218 | Options: 219 | -C , --directory= Change to before doing anything. 220 | --completion= Output code to activate task completions. 221 | Supported shells: 'bash'. 222 | -f , --file= Use as a Baskfile. 223 | -l, --list-tasks List available tasks. 224 | -h, --help Print this message and exit. 225 | ``` 226 | 227 | ### Flags 228 | 229 | All flags you pass after the task names are passed to your tasks. 230 | 231 | ```bash 232 | task_foo() { 233 | echo ${@} 234 | } 235 | 236 | $ bask foo --production 237 | --production 238 | ``` 239 | 240 | To pass options to the `bask` CLI specifically, you must provide them 241 | before any task names: 242 | 243 | ```bash 244 | $ bask -f scripts/tasks.sh foo 245 | ``` 246 | 247 | ### Tab Completion 248 | 249 | The `bask` CLI supports autocompletion for task names (bash only). Simply add 250 | the following line your `~/.bashrc`: 251 | 252 | ```shell 253 | eval $(bask --completion=bash) 254 | ``` 255 | 256 | 257 | ## API 258 | 259 | This section covers all the features of Bask. 260 | 261 | ### Baskfile 262 | 263 | Your Baskfile can be named any of the following. Using a `.sh` suffix helps with 264 | things like editor syntax highlighting. 265 | 266 | ``` 267 | Baskfile 268 | Baskfile.sh 269 | baskfile 270 | baskfile.sh 271 | ``` 272 | 273 | 274 | #### Default Tasks 275 | 276 | You can specify a default task in your Baskfile. It will run when no arguments 277 | are provided. There are two ways to do this: 278 | 279 | ```shell 280 | task_default() { 281 | # do something ... 282 | } 283 | ``` 284 | 285 | ```shell 286 | bask_default_task="foo" 287 | task_foo() { 288 | # do something ... 289 | } 290 | ``` 291 | 292 | 293 | ### Methods 294 | 295 | Bask exposes a number of functions for manipulating dependencies among tasks, 296 | for logging, and for a few utilities. 297 | 298 | #### `bask_depends` 299 | 300 | > Alias: `bask_sequence` 301 | 302 | This function is for declaring dependencies of a task. It should be invoked 303 | within another task. 304 | 305 | Usage: `bask_depends [task ...]` 306 | 307 | ```shell 308 | task_default() { 309 | bask_depends foo bar 310 | # Output: 311 | # [21:50:33.194] Starting 'foo' 312 | # [21:50:33.195] Finished 'foo' after 1 ms 313 | # [21:50:33.196] Starting 'bar' 314 | # [21:50:33.198] Finished 'bar' after 2 ms 315 | } 316 | ``` 317 | 318 | If any task return non-zero, the entire sequence of tasks is aborted with 319 | an error. 320 | 321 | Note that return codes can be bubbled up using `... || return`, so you can 322 | conveniently abort tasks prematurely like this: 323 | 324 | ``` 325 | maybe_error() { 326 | return $(($RANDOM % 2)) 327 | } 328 | 329 | task_try() { 330 | echo "Bad" 331 | maybe_error || return # <-- bubbles error up if error 332 | # code for when no error 333 | } 334 | 335 | task_finally() { 336 | echo "Good" 337 | } 338 | 339 | task_foo() { 340 | bask_depends try finally 341 | } 342 | ``` 343 | 344 | #### `bask_fork_join` 345 | 346 | > Alias: `bask_parallel` 347 | 348 | Bask also allows for spawning independent work in parallel, and resuming the 349 | task when all tasks have completed: 350 | 351 | Usage: `bask_fork_join [task ...]` 352 | 353 | ```shell 354 | task_default() { 355 | bask_fork_join sleep3 sleep5 356 | bask_log "after" 357 | # [21:50:33.194] Starting 'sleep3' 358 | # [21:50:33.194] Starting 'sleep5' 359 | # [21:50:36.396] Finished 'sleep3' after 3.20 s 360 | # [21:50:38.421] Finished 'sleep5' after 5.23 s 361 | # [21:50:38.422] after 362 | } 363 | ``` 364 | 365 | Note that all tasks always run to completion, unlike with `bask_depends`. 366 | 367 | #### `bask_run` 368 | 369 | This will log a timestamp plus the command with its arguments, then run it. 370 | 371 | Usage: `bask_run ` 372 | 373 | Note that the command must be a simple command--things like pipes, `&&`, `||`, 374 | `{ ... }`, etc. will not work. 375 | 376 | 377 | #### `bask_log` 378 | 379 | You can log information inside Bask tasks using one of the five `bask_log` 380 | helpers: 381 | 382 | | Function | Description | 383 | | --- | --- | 384 | | `bask_log` | Adds a log line (with time), in the foreground color | 385 | | `bask_log_success` | Same as above, but in green | 386 | | `bask_log_error` | Same as above, but in red | 387 | | `bask_log_warning` | Same as above, but in yellow | 388 | | `bask_log_info` | Same as above, but in cyan | 389 | | `bask_log_debug` | Same as above, but in gray | 390 | 391 | Usage: `bask_log message` 392 | 393 | All logging functions have the same usage. 394 | 395 | #### `bask_colorize` 396 | 397 | While the dedicated logging functions are helpful, sometimes you want finer 398 | control over your colors. 399 | 400 | Usage: `bask_colorize message` 401 | 402 | Where `colorname` is one of 403 | 404 | | | | | | | | 405 | | --- | --- | --- | --- | --- | --- | 406 | | black | gray | light_gray | white | | | 407 | | red | green | yellow | blue | purple | cyan | 408 | | light_red | light_green | light_yellow | light_blue | light_purple | light_cyan | 409 | 410 | ```shell 411 | # Simple example 412 | bask_colorize purple This will all be purple 413 | 414 | # Use with `bask_log` to get a timestamp: 415 | bask_log "$(bask_colorize purple This will all be purple)" 416 | ``` 417 | 418 | Note that your message will be wrapped with the appropriate color **and** reset 419 | codes. You don't need to worry about manually turning the color back to normal. 420 | 421 | #### `bask_list_tasks` 422 | 423 | The default behavior if no tasks are specified at the command line and no 424 | default tasks are registered is to list all available tasks. You can manually 425 | invoke that with this function. 426 | 427 | Usage: `bask_list_tasks` 428 | 429 | ```shell 430 | task_list() { 431 | bask_list_tasks 432 | } 433 | ``` 434 | 435 | #### `bask_is_task_defined` 436 | 437 | Utility function for checking whether a list of tasks are defined. Used 438 | internally, but exposed externally. 439 | 440 | Usage: `bask_is_task_defined [task ...]` 441 | 442 | Note that you can pass in more than one task for checking. 443 | 444 | Alternatively, you can use `bask_is_task_defined_verbose`, which will do the 445 | same checks, but log an error if any task is not defined. 446 | 447 | 448 | ## Contribute 449 | 450 | Bask was forked from [bash-task-runner]. Chances are that if you have an issue 451 | or pull request it can be made against that upstream repo. 452 | 453 | If your issue pertains to functionality specifically only provided here, then 454 | feel free to voice your concerns here. If you're confused where to make the 455 | request, feel free to ask in [Gitter][gitter] first. 456 | 457 | ## License 458 | 459 | GNU Lesser General Public License v3 (LGPL-3.0). See License.md 460 | 461 | | Copyright (c) | Commits | 462 | | --------------- | -------- | 463 | | Aleksej Komarov | 8a28f72 | 464 | | Jake Zimmerman | 5e92344+ | 465 | 466 | 467 | 468 | [bash-task-runner]: https://github.com/stylemistake/bash-task-runner 469 | [gitter]: https://gitter.im/jez/bask 470 | [submodules]: https://git-scm.com/book/en/v2/Git-Tools-Submodules 471 | -------------------------------------------------------------------------------- /bin/bask: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Get paths where source files are located. http://stackoverflow.com/a/42918 4 | BINDIR="$(dirname "$(perl -MCwd -le 'print Cwd::abs_path(shift)' "${BASH_SOURCE[0]}")")" 5 | ROOTDIR="$(dirname "$BINDIR")" 6 | SRCDIR="$ROOTDIR/src" 7 | 8 | if [[ "${FUNCNAME[0]}" == source ]]; then 9 | echo "Can't source the 'bask' executable. Instead, source 'src/bask.sh' directly." 10 | exit 1 11 | fi 12 | 13 | # Include core files 14 | # shellcheck disable=SC1090 15 | source "$SRCDIR/bask.sh" 16 | 17 | # Include CLI specific files 18 | # shellcheck disable=SC1090 19 | source "$SRCDIR/cli.sh" 20 | -------------------------------------------------------------------------------- /completion/bask.bash: -------------------------------------------------------------------------------- 1 | _bask_completions() { 2 | local -a options=('-C' '-f' '-l' '-h' '--directory=' '--file=' 3 | '--list-tasks' '--help') 4 | local cur="${COMP_WORDS[COMP_CWORD]}" 5 | local prev="${COMP_WORDS[COMP_CWORD-1]}" 6 | local flag tasks 7 | ## Complete the short directory option 8 | if [[ ${prev} == '-C' ]]; then 9 | compopt -o dirnames 10 | COMPREPLY=() 11 | return 0 12 | fi 13 | ## Complete the short file option 14 | if [[ ${prev} == '-f' ]]; then 15 | COMPREPLY=() 16 | return 0 17 | fi 18 | ## Shift compwords to feed into the next condition 19 | if [[ ${prev} == '--'* && ${cur} == '=' ]]; then 20 | flag="${prev}" 21 | prev="${cur}" 22 | cur="" 23 | fi 24 | ## Complete long options 25 | if [[ ${prev} == '=' ]]; then 26 | [[ -z ${flag} ]] && flag="${COMP_WORDS[COMP_CWORD-2]}" 27 | ## Limit selection to directories 28 | if [[ ${flag} == '--directory' ]]; then 29 | compopt -o dirnames 30 | fi 31 | ## Show file completions (using -o default) 32 | COMPREPLY=() 33 | return 0 34 | fi 35 | ## Complete all options 36 | if [[ ${cur} == '-'* ]]; then 37 | COMPREPLY=($(compgen -W "${options[*]}" -- "${cur}")) 38 | [[ ${#COMPREPLY[@]} == 1 && ${COMPREPLY[0]} != "--"*"=" ]] \ 39 | && compopt +o nospace || compopt -o nospace 40 | return 0 41 | fi 42 | ## NOTE: ${COMP_WORDS[@]:1} provides the context of currently entered 43 | ## options, so bask would know what Baskfile to use. 44 | tasks="$(bask -l ${COMP_WORDS[@]:1})" 45 | if [[ ${?} -ne 0 ]]; then 46 | ## Show nothing at all 47 | compopt +o default 48 | COMPREPLY=() 49 | return 0 50 | fi 51 | ## Complete task names 52 | COMPREPLY=($(compgen -W "${tasks}" -- "${cur}")) 53 | } 54 | 55 | complete -o default -F _bask_completions bask 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bask", 3 | "version": "0.9.0", 4 | "description": "A task runner for bash", 5 | "directories": { 6 | "test": "test" 7 | }, 8 | "bin": { 9 | "bask": "bin/bask" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/jez/bask.git" 14 | }, 15 | "keywords": [ 16 | "bash", 17 | "task", 18 | "runner", 19 | "bask" 20 | ], 21 | "license": "LGPL-3.0", 22 | "bugs": { 23 | "url": "https://github.com/jez/bask/issues" 24 | }, 25 | "homepage": "https://github.com/jez/bask#readme" 26 | } 27 | -------------------------------------------------------------------------------- /src/bask.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # shellcheck disable=SC2034 4 | VERSION="0.9.0" 5 | 6 | ## Only use milliseconds if available, by detecting GNU coreutils 7 | ## See: http://stackoverflow.com/a/8748344/319952 8 | if date --version > /dev/null 2>&1 ; then 9 | MILLIS="%3N" 10 | else 11 | # Approximate the milliseconds with zeros 12 | MILLIS="000" 13 | fi 14 | 15 | # Suffix for logging purposes 16 | MILLI_SUFFIX=".$MILLIS" 17 | 18 | ## Default task 19 | declare -g bask_default_task="default" 20 | 21 | ## Trap EXIT signal to bootstrap bask. 22 | ## Works like a charm - your script ends, tasks start to run. 23 | ## Trap resets after bootstrapping. 24 | trap '[[ ${?} -eq 0 ]] && _bask_bootstrap' EXIT 25 | 26 | ## Expand aliases 27 | shopt -s expand_aliases 28 | 29 | ## Determine the initial passed arguments to the root script 30 | declare -ga bask_args=("${@}") 31 | 32 | ## Split arguments into tasks and flags. 33 | ## All flags are then passed on to tasks. 34 | ## E.g. --production 35 | ## NOTE: The actual splitting is done in _bask_bootstrap. 36 | declare -ga bask_flags 37 | declare -ga bask_tasks 38 | 39 | declare -gA _bask_colors=( 40 | [black]="$(echo -e '\e[30m')" 41 | [red]="$(echo -e '\e[31m')" 42 | [green]="$(echo -e '\e[32m')" 43 | [yellow]="$(echo -e '\e[33m')" 44 | [blue]="$(echo -e '\e[34m')" 45 | [purple]="$(echo -e '\e[35m')" 46 | [cyan]="$(echo -e '\e[36m')" 47 | [light_gray]="$(echo -e '\e[37m')" 48 | [gray]="$(echo -e '\e[90m')" 49 | [light_red]="$(echo -e '\e[91m')" 50 | [light_green]="$(echo -e '\e[92m')" 51 | [light_yellow]="$(echo -e '\e[93m')" 52 | [light_blue]="$(echo -e '\e[94m')" 53 | [light_purple]="$(echo -e '\e[95m')" 54 | [light_cyan]="$(echo -e '\e[96m')" 55 | [white]="$(echo -e '\e[97m')" 56 | [reset]="$(echo -e '\e[0m')" 57 | ) 58 | 59 | ## Logs a message with a timestamp 60 | bask_log() { 61 | local timestamp 62 | timestamp="$(date "+%T$MILLI_SUFFIX")" 63 | echo "[${_bask_colors[gray]}${timestamp}${_bask_colors[reset]}] ${*}" 64 | } 65 | 66 | ## Variations of log with colors 67 | bask_log_error() { 68 | bask_log "${_bask_colors[red]}${*}${_bask_colors[reset]}" 69 | } 70 | 71 | bask_log_warning() { 72 | bask_log "${_bask_colors[yellow]}${*}${_bask_colors[reset]}" 73 | } 74 | 75 | bask_log_success() { 76 | bask_log "${_bask_colors[green]}${*}${_bask_colors[reset]}" 77 | } 78 | 79 | bask_log_info() { 80 | bask_log "${_bask_colors[cyan]}${*}${_bask_colors[reset]}" 81 | } 82 | 83 | bask_log_debug() { 84 | bask_log "${_bask_colors[gray]}${*}${_bask_colors[reset]}" 85 | } 86 | alias bask_log_notice=bask_log_debug 87 | 88 | # Helper colorization function. Usage: 89 | # 90 | # bask_colorize purple This will all be purple 91 | # 92 | # You may want to use in conjunction with bask_log to get a timestamp: 93 | # 94 | # bask_log "$(bask_colorize purple This will all be purple)" 95 | # 96 | bask_colorize() { 97 | echo "${_bask_colors[$1]}${*:2}${_bask_colors[reset]}" 98 | } 99 | 100 | ## List all defined functions beginning with `task_` 101 | _bask_get_defined_tasks() { 102 | local IFS=$'\n' 103 | for task in $(typeset -F); do 104 | [[ ${task} == 'declare -f task_'* ]] && echo "${task:16}" 105 | done 106 | } 107 | 108 | ## Fancy wrapper for `_bask_get_defined_tasks` 109 | bask_list_tasks() { 110 | bask_log "Available tasks:" 111 | local -a tasks=($(_bask_get_defined_tasks)) 112 | if [[ ${#tasks[@]} -eq 0 ]]; then 113 | bask_log " ${_bask_colors[light_gray]}${_bask_colors[reset]}" 114 | return 115 | fi 116 | for task in "${tasks[@]}"; do 117 | bask_log " ${_bask_colors[cyan]}${task}${_bask_colors[reset]}" 118 | done 119 | } 120 | 121 | ## Returns a human readable duration in ms 122 | _bask_pretty_ms() { 123 | local -i ms="${1}" 124 | local result 125 | ## If zero or nothing 126 | if [[ -z ${ms} || ${ms} -lt 1 ]]; then 127 | echo "0 ms" 128 | return 129 | ## Only ms 130 | elif [[ ${ms} -lt 1000 ]]; then 131 | echo "${ms} ms" 132 | return 133 | ## Only seconds with trimmed ms point 134 | elif [[ ${ms} -lt 60000 ]]; then 135 | result=$((ms / 1000 % 60)).$((ms % 1000)) 136 | echo "${result:0:4} s" 137 | return 138 | fi 139 | local -i parsed 140 | ## Days 141 | parsed=$((ms / 86400000)) 142 | [[ ${parsed} -gt 0 ]] && result="${result} ${parsed} d" 143 | ## Hours 144 | parsed=$((ms / 3600000 % 24)) 145 | [[ ${parsed} -gt 0 ]] && result="${result} ${parsed} h" 146 | ## Minutes 147 | parsed=$((ms / 60000 % 60)) 148 | [[ ${parsed} -gt 0 ]] && result="${result} ${parsed} m" 149 | ## Seconds 150 | parsed=$((ms / 1000 % 60)) 151 | [[ ${parsed} -gt 0 ]] && result="${result} ${parsed} s" 152 | ## Output result 153 | echo "${result}" 154 | } 155 | 156 | ## Checks if program is accessible from current $PATH 157 | _bask_is_defined() { 158 | hash "${@}" 2>/dev/null 159 | } 160 | 161 | bask_is_task_defined() { 162 | for task in "${@}"; do 163 | _bask_is_defined "task_${task}" || return 164 | done 165 | } 166 | 167 | bask_is_task_defined_verbose() { 168 | for task in "${@}"; do 169 | if ! _bask_is_defined "task_${task}"; then 170 | bask_log_error "Task '${task}' is not defined!" 171 | return 1 172 | fi 173 | done 174 | } 175 | 176 | _bask_run_task() { 177 | local -i time_start time_end 178 | local time_diff 179 | local task_color="${_bask_colors[cyan]}${1}${_bask_colors[reset]}" 180 | bask_log "Starting '${task_color}'..." 181 | time_start="$(date +%s$MILLIS)" 182 | "task_${1}" "${bask_flags[@]}" 183 | local exit_code=${?} 184 | time_end="$(date +%s$MILLIS)" 185 | time_diff="$(_bask_pretty_ms $((time_end - time_start)))" 186 | if [[ ${exit_code} -ne 0 ]]; then 187 | bask_log_error "Task '${1}'" \ 188 | "failed after ${time_diff} (${exit_code})" 189 | return ${exit_code} 190 | fi 191 | bask_log "Finished '${task_color}'" \ 192 | "after ${_bask_colors[purple]}${time_diff}${_bask_colors[reset]}" 193 | } 194 | 195 | ## Run tasks sequentially. 196 | bask_sequence() { 197 | bask_is_task_defined_verbose "${@}" || return 198 | for task in "${@}"; do 199 | _bask_run_task "${task}" || return 1 200 | done 201 | } 202 | alias bask_depends=bask_sequence 203 | 204 | ## Run tasks in parallel. 205 | bask_parallel() { 206 | bask_is_task_defined_verbose "${@}" || return 1 207 | local -a pid 208 | local -i exits=0 209 | for task in "${@}"; do 210 | _bask_run_task "${task}" & pid+=(${!}) 211 | done 212 | for pid in "${pid[@]}"; do 213 | wait "${pid}" || exits+=1 214 | done 215 | [[ ${exits} -eq 0 ]] && return 0 216 | [[ ${exits} -lt ${#} ]] && return 41 || return 42 217 | } 218 | alias bask_fork_join=bask_parallel 219 | 220 | ## Output command before execution 221 | bask_run() { 222 | bask_log_notice "${@}" 223 | "${@}" 224 | } 225 | 226 | ## Starts the initial task. 227 | _bask_bootstrap() { 228 | ## Clear a trap we set up earlier 229 | trap - EXIT 230 | ## Parse arguments 231 | for arg in "${bask_args[@]}"; do 232 | if [[ ${arg} == -* ]]; then 233 | bask_flags+=("${arg}") 234 | else 235 | bask_tasks+=("${arg//-/_}") 236 | fi 237 | done 238 | ## Run tasks 239 | if [[ ${#bask_tasks[@]} -gt 0 ]]; then 240 | bask_sequence "${bask_tasks[@]}" || exit ${?} 241 | return 0 242 | fi 243 | if bask_is_task_defined "${bask_default_task}"; then 244 | _bask_run_task "${bask_default_task}" || exit ${?} 245 | return 0 246 | fi 247 | ## Nothing to run 248 | bask_list_tasks 249 | } 250 | -------------------------------------------------------------------------------- /src/cli.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## NOTE: This script depends on bask.sh to be loaded first. 4 | 5 | ## Globals that come from the entry points 6 | declare -g SRCDIR 7 | declare -g VERSION 8 | 9 | ## Baskfile names that CLI will be looking for in current directory. 10 | declare -ga baskfile_default_names=( 11 | 'baskfile.sh' 12 | 'Baskfile.sh' 13 | 'baskfile' 14 | 'Baskfile' 15 | ) 16 | 17 | ## Global variables that hold CLI settings 18 | declare -g bask_file 19 | declare -g bask_directory 20 | declare -g bask_list_tasks 21 | 22 | ## Outputs an error message and exits the script 23 | bask_cli_error() { 24 | trap - EXIT 25 | bask_log_error "${@}" 26 | exit 2 27 | } 28 | 29 | ## Outputs a nice help message 30 | bask_cli_help() { 31 | trap - EXIT 32 | echo "Usage: ${0} [options] [task] [task_options] ..." 33 | echo "Version: $VERSION" 34 | echo "Options:" 35 | echo " -C , --directory= Change to before doing anything." 36 | echo " --completion= Output code to activate task completions." 37 | echo " Supported shells: 'bash'." 38 | echo " -f , --file= Use as a Baskfile." 39 | echo " -l, --list-tasks List available tasks." 40 | echo " -h, --help Print this message and exit." 41 | exit 0 42 | } 43 | 44 | ## Outputs a list of tasks 45 | bask_cli_list_tasks() { 46 | trap - EXIT 47 | _bask_get_defined_tasks 48 | exit 0 49 | } 50 | 51 | ## Outputs code to activate task completions 52 | bask_cli_get_completions_code() { 53 | trap - EXIT 54 | local shell="${1:-bash}" 55 | echo "source $SRCDIR/../completion/bask.${shell}" 56 | exit 0 57 | } 58 | 59 | ## Parses CLI-specific flags. 60 | ## Must take "${bask_args[@]}" as the argument. 61 | bask_cli_parse_args() { 62 | ## Clean up currently defined arguments 63 | bask_args=() 64 | ## Iterate over the provided arguments 65 | while [[ ${#} -gt 0 ]]; do 66 | ## Stop parsing after the first non-flag argument 67 | if [[ ${1} != -* ]]; then 68 | break 69 | fi 70 | ## Help message 71 | if [[ ${1} == '-h' || ${1} == '--help' ]]; then 72 | bask_cli_help 73 | fi 74 | ## List tasks 75 | if [[ ${1} == '-l' || ${1} == '--list-tasks' ]]; then 76 | bask_list_tasks="true" 77 | fi 78 | ## Return the completions code 79 | if [[ ${1} == '--completion='* ]]; then 80 | bask_cli_get_completions_code "${1#*=}" 81 | fi 82 | ## Baskfile override 83 | if [[ ${1} == '-f' ]]; then 84 | [[ -z ${2} ]] && bask_cli_error "Missing an argument after ${1}" 85 | bask_file="${2}" 86 | shift 2 87 | continue 88 | fi 89 | if [[ ${1} == '--file='* ]]; then 90 | bask_file="${1#*=}" 91 | shift 1 92 | continue 93 | fi 94 | ## Current directory override 95 | if [[ ${1} == '-C' ]]; then 96 | [[ -z ${2} ]] && bask_cli_error "Missing an argument after ${1}" 97 | bask_directory="${2}" 98 | shift 2 99 | continue 100 | fi 101 | if [[ ${1} == '--directory='* ]]; then 102 | bask_directory="${1#*=}" 103 | shift 1 104 | continue 105 | fi 106 | ## Append unclassified flags back to bask_args 107 | bask_args+=("${1}") 108 | shift 1 109 | done 110 | ## Append remaining arguments that will be passed to the 111 | ## bootstrap function 112 | bask_args+=("${@}") 113 | } 114 | 115 | ## Parse the actual arguments 116 | bask_cli_parse_args "${bask_args[@]}" 117 | 118 | ## Try to change the current directory 119 | if [[ -n ${bask_directory} ]]; then 120 | if [[ ! -d ${bask_directory} ]]; then 121 | bask_cli_error "'${bask_directory}' is not a directory!" 122 | fi 123 | cd "${bask_directory}" || bask_cli_error "Could not change directory!" 124 | fi 125 | 126 | ## Try to find a Baskfile 127 | if [[ -n ${bask_file} ]]; then 128 | if [[ ! -f ${bask_file} ]]; then 129 | bask_cli_error "'${bask_file}' is not a file!" 130 | fi 131 | else 132 | for file in "${baskfile_default_names[@]}"; do 133 | if [[ -f ${file} ]]; then 134 | bask_file="${file}" 135 | break 136 | fi 137 | done 138 | fi 139 | 140 | ## Baskfile not found 141 | if [[ -z ${bask_file} ]]; then 142 | bask_cli_error 'No Baskfile found.' 143 | fi 144 | 145 | ## Source the Baskfile 146 | # shellcheck disable=SC1090 147 | source "${bask_file}" 148 | 149 | if [ -n "${bask_list_tasks}" ]; then 150 | bask_cli_list_tasks 151 | fi 152 | -------------------------------------------------------------------------------- /test/Baskfile.sh: -------------------------------------------------------------------------------- 1 | ## Sample Baskfile 2 | 3 | task_one() { 4 | bask_parallel end 5 | } 6 | 7 | task_two() { 8 | bask_parallel one end 9 | } 10 | 11 | task_three() { 12 | bask_parallel one two end 13 | } 14 | 15 | task_end() { 16 | # do nothing 17 | echo -n 18 | } 19 | 20 | task_default() { 21 | bask_parallel one two three end 22 | } 23 | -------------------------------------------------------------------------------- /test/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | cd `dirname ${0}` 3 | 4 | source src/bask.sh 5 | source Baskfile.sh 6 | --------------------------------------------------------------------------------