{"id":24332578,"url":"https://github.com/binaryphile/task.bash","last_synced_at":"2025-10-08T13:49:25.449Z","repository":{"id":271651615,"uuid":"914145193","full_name":"binaryphile/task.bash","owner":"binaryphile","description":"harmonize your Unix work environments","archived":false,"fork":false,"pushed_at":"2025-09-02T14:04:28.000Z","size":407,"stargazers_count":4,"open_issues_count":13,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-09-27T18:37:34.973Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Shell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/binaryphile.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-01-09T03:07:19.000Z","updated_at":"2025-04-08T14:52:23.000Z","dependencies_parsed_at":"2025-02-23T22:18:27.309Z","dependency_job_id":"7f6d1434-87ad-4aa0-901a-b42378af26cf","html_url":"https://github.com/binaryphile/task.bash","commit_stats":null,"previous_names":["binaryphile/task.bash"],"tags_count":18,"template":false,"template_full_name":null,"purl":"pkg:github/binaryphile/task.bash","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/binaryphile%2Ftask.bash","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/binaryphile%2Ftask.bash/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/binaryphile%2Ftask.bash/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/binaryphile%2Ftask.bash/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/binaryphile","download_url":"https://codeload.github.com/binaryphile/task.bash/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/binaryphile%2Ftask.bash/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":278956337,"owners_count":26075221,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-08T02:00:06.501Z","response_time":56,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-01-18T02:28:53.181Z","updated_at":"2025-10-08T13:49:25.428Z","avatar_url":"https://github.com/binaryphile.png","language":"Shell","funding_links":[],"categories":[],"sub_categories":[],"readme":"# task.bash -- harmonize your Unix work environments with idempotent tasks\n\n![version](assets/version.svg) ![lines](assets/lines.svg) ![tests](assets/tests.svg) ![coverage](assets/coverage.svg)\n\nCreate your work environment that follows you everywhere. Keep up to date via integration\nwith your workflow.  Idempotency allows one script to keep multiple machines in sync.\n\n**Requires Bash 5**\n\n![update-env](assets/update-env.gif)\n\nWith task.bash, you capture your configuration in a shell script.  We all know that writing\na script to install some software packages is easy.  Writing one that both configures a\nfresh system, then updates it later is more difficult.  Once accomplished though, you have a\nsingle environment that follows you everywhere, to any machine.\n\nTask.bash assists by making it easy to:\n\n- transform shell commands into idempotent tasks\n\n- drive configuration changes through your script, maintaining it as the source of truth\n\n- work with components that require shell setup, such as needing to source a provided\n    script or modify PATH\n\n- work with components that are too new or difficult for less flexible configuration\n    solutions\n\nThat means you can easily synchronize your environments across machines. Create a\nconfiguration script that lives in a git repository.  New machines run the script by\ncurl-piping it from GitHub.  The repo is then cloned to the machine and gets updates from\ngit.  *You* are the synchronization mechanism, in tandem with git.  Your system changes when\nyou tell it to, much like when you run package upgrades.  Run your script when you would\nhave upgraded via the package manager in the past, or more often.\n\nUse it to:\n\n- install software\n- clone git repositories\n- make symlinks\n- change directory ownership\n- any of Bash's other greatest hits\n\nOther features:\n\n- command output suppression\n- human-friendly ongoing status reporting\n- post-run summarization\n- user interaction, e.g. the ability to prompt for a password\n- ad-hoc scripting\n\n## Installation\n\nClone or copy `task.bash` where it can be sourced by your script.\n\nAlternatively, vendor it into your configuration script by pasting it in place of the\n`source` command.\n\n## Getting Started\n\nTask.bash relies on the concept of tasks, where a task is a Bash function that is\nidempotent.\n\nIdempotence simply means that the task results in the same outcome if it is run once or a\nhundred times.  In this case, it means that no matter the state of your system, it will be\nbrought to the same, current specification when the script is run.  Bash commands aren't\nnecessarily idempotent by default, so task.bash helps you make them so.\n\n## Anatomy of a task\n\n```bash\ncloneDotfilesTask() {           # \u003c== task name, ends with Task by convention\n  desc  'clone my dotfiles'     # \u003c== what shows up in the output for this task\n  ok    '[[ -e ~/dotfiles ]]'   # \u003c== don't run the command if \"ok\" evals true -- idempotency!\n\n  cmd   'git clone git@github.com:user/dotfiles ~/dotfiles'\n}\n```\n\nWhen this task is run, it clones user's dotfiles from GitHub into `~/dotfiles` and reports\nthe task as `[changed]`.  If `~/dotfiles` already exists, however, it is reported as `[ok]`\nand is not run.\n\nEach keyword (`desc`, `ok`, `cmd`) is a function that configures the task.  `cmd` does\ndouble-duty, defining the task's command as well as running it. Because it needs the rest of\nthe task definition, `cmd` *must* be the last line of the definition.\n\nBy convention, `desc` is the first line, serving as a comment to describe the task.  All\nother task.bash keywords can come between `desc` and `cmd` in any order.\n\nThe `ok` keyword tells task.bash how to tell if the task is satisfied.  `ok` can take a\nsimple `test` expression, or arbitrary code.  It is evaluated before the command.  If the\ncode evaluates to true (return code 0), the task is already satisfied and does not continue.\nOtherwise, the command is run.  If it is run, the condition is checked afterward and this\ntime, if it does not pass, the task is marked `[failed]` and the script stops.\n\nNotice that each keyword takes one argument.  `ok` and `cmd` both contain code, and\ntask.bash `eval`s that code, so expansions like `~/dotfiles` are taken care of.  *For\nsecurity, do not populate user input into these fields.*\n\n## Additional Task Keywords\n\nThe rest of task.bash's features lie in the remaining keywords.  Each of these can appear in\na task definition:\n\n- `exist PATH` -- ok if PATH exists, alternative to `ok`\n- `prog on|off` -- show command output as it runs\n- `runas USER` -- run the command as the user USER, using sudo\n- `unchg TEXT` -- look for TEXT in command output and if present, mark the task `[ok]`\n\nWe'll revisit these as we define examples.\n\n## Task.bash Functions\n\nWhile the keywords already described are technically functions, we call them keywords to\ndistinguish them from the naming used for other task.bash functions.\n\nMost Bash code you will see uses snake_case for functions and variables.  That's fine, but\nwhen using libraries, namespacing matters.  A naming scheme like Go's integrates better with\na namespace already inhabited by environment variables and third-party code.\n\nBy convention, task.bash uses function names like `task.SetShortRun`, where the function\nname is PascalCased.  The name is also prefixed with `task` so task.bash's functions won't\nconflict with your function names.\n\nThere are a couple of functions in addition to the task keywords we've seen already:\n\n- `task.Platform` - returns `macos` on mac, otherwise `linux`\n- `task.Summarize` - summarize the results of the run\n- `task.SetShortRun on|off` - skip long tasks (tasks with `prog` or `unchg`)\n\n`task.Platform` is intended to be used to conditionally perform tasks based on the current\nplatform.\n\n`task.Summarize` should be part of any script, run after all of the tasks to report what\nhappened.\n\n`task.SetShortRun` allows your script to take an option that tells task.bash to skip\nlong-running tasks.  Any task marked with `prog` or `unchg` is considered long-running\nautomatically.\n\n### Configuration Script Outline\n\nA configuration script has two parts: one that defines tasks and the other that runs them.\n\nWe'll call this script `update-env`:\n\n```bash\n#!/usr/bin/env bash\n\nmain() {    # \u003c== using main lets us put it here up top, where it belongs\n  # do tasks\n  cloneDotfilesTask\n\n  # summarize the results in output\n  task.Summarize\n}\n\ncloneDotfilesTask() { ... }\n\n# boilerplate\nsource /path/to/task.bash\nmain\n```\n\n`chmod +x update-env` the file so we can run it in a bit.\n\n## Running Tasks\n\nBefore we run the script, however, we need to add one more thing, to enable Bash strict\nmode.  Bash strict mode allows the script to stop when errors occur and to flag unset\nvariable references, both of which are suited to scripting.  By convention, we set it at the\nbeginning of `main`:\n\n```bash\nmain() {\n  set -euo pipefail\n  cloneDotFilesTask\n  task.Summarize\n}\n```\n\nWe strongly advise you to employ strict mode.  Otherwise error conditions may allow\nexecution of unintended codepaths or further errors to occur.  When dealing with\nsystem-level configuration, that's risky.\n\nNow, here's the output from running the script:\n\n```bash\n[changed]       clone dotfiles from github\n\n[summary]\nok:      0\nchanged: 1\n```\n\nThe responses are actually color-coded, green for `[ok]` and `[changed]` and red for\n`[failed]`.\n\nNotice first that the output of the command is suppressed.  This is so the task readout is\nconcise.\n\nHowever, sometimes a command may take time, perhaps more than expected.  For this reason,\nbefore the command is run, there is a line of output showing the `[begin]` status for the\ntask, but that line is overwritten by the result once it is available.  The `[begin]` status\nline does not show up in the output above.\n\nIf you run the script when the directory exists already, the output will report the `[ok]`\nstatus instead of `[changed]` and nothing will be run.\n\nWhen a command fails, execution stops and it is reported.  You are shown stdout and stderr\ncombined for debugging purposes.  If the command reports success, but the\n`ok` condition fails anyway, the task is reported as failed.\n\n## Defining Tasks\n\nThe goal of most tasks is to make a command idempotent.  What that means can vary from\ncommand to command.  We'll take a look at how you might want to approach different kinds of\ntasks.\n\n### Speculative commands\n\nSome commands are designed to update the system based on external input, such as the package\nmanager.  If a package has a new version, the package manager installs it, otherwise it does\nnothing.\n\nCommands such as this are speculative; they have to check elsewhere before determining what\nto do, if anything.  When running the upgrade, we don't know if we should be expecting a new\npackage version or not...the package manager has to tell us. That means:\n\n- there is no way to specify an `ok` expression for them a priori\n- task.bash must look at the command output to tell what status to report, `ok` or `changed`\n\nHere is a task for `apt upgrade`:\n\n```bash\naptUpgradeTask() {\n  desc  'upgrade system packages'\n  prog  on\n  runas root\n  unchg '0 upgraded, 0 newly installed'\n\n  cmd   'apt update -qq \u0026\u0026 apt upgrade -y'\n}\n```\n\nWe're seeing new keywords here, `prog`, `runas` and `unchg`:\n\n#### `prog on` enables command output\n\n`apt` can take some time.  If there is no output, your script can seem frozen. `prog on`\nenables command output to tell you when the command is making progress.  Such output starts\nwith `[progress]`.\n\n#### `runas USER` runs the command as USER\n\n`apt` needs root permissions to modify the system.  `runas root` tells task.bash to use\n`sudo` to run the command as root user.\n\n#### `unchg TEXT` tells task.bash whether the command made changes\n\n`apt` conveniently reports whether packages were installed or updated. `unchg` looks for\nthat message and marks the task `[ok]` if we see it, otherwise `[changed]`.\n\n### Complex commands\n\n```bash\ncurlTask() {\n  desc   'download coolscript from github'\n  exist  ~/.local/bin/coolscript\n\n  cmd    '\n    mkdir -p ~/.local/bin\n    curl -fsSL git@github.com:user/coolscript \u003e~/.local/bin/coolscript\n  '\n}\n```\n\nWe saw that `cmd` can take commands like `apt update -qq \u0026\u0026 apt upgrade -y` but you can use\n`cmd` with arbitrary Bash, including multi-line scripts, pipelines, you name it.\n\nMultiline quotes are convenient in this case, but should the code be more than a few lines,\nyou'll probably want to put it in its own function and call that with `cmd` instead.\n\nWe also see here the `exist` keyword:\n\n#### `exist PATH` sets ok with the Bash -e path existence test\n\n`exist` sets `ok` with a path existence test, meaning you only need to specify one of `ok`\nor `exist`.  It is a frequently-useful test, so task definitions benefit from the more\nreadable `exist`.\n\n### Parameterized tasks\n\nSo far, no task has taken parameters, which makes them hard-coded to things like filenames.\nMany tasks are generic enough to be reusable, if they only could take parameters.  Parameter\nhandling with tasks is a bit tricky though, since controlling the timing of evaluation is\nimportant.\n\nTask.bash comes with a handful of parameterized tasks, such as `task.GitClone` and\n`task.Ln`.\n\nHere's a simple example of how to write one:\n\n```bash\nmkdirTask() {\n  local dir=$1\n  desc  \"make directory $dir\"\n  cmd   \"mkdir -p $dir\"\n}\n```\n\nFirst, notice that we're taking the first argument as `dir` and using it in the task\ndefinition.  In order to expand it, we've used double-quotes instead of single.\n\nThis works for simple cases but becomes difficult with complexity and edge cases.  For\nexample, this will not handle directories with spaces as it stands, since expansion will\nhappen here, and then evaluation by `cmd` will not have the command properly quoted.\n\nWe could try to fix it by single-quoting `dir` within the double-quotes, but then it becomes\nsensitive to single-quotes in `dir`'s value.  `printf %q` is another option to make the\nvalue eval-safe, but rather than try to quote our way out of it, there's a more readable\noption. Let's define a new function within the task and call that.\n\n```bash\nmkdirTask() {\n  local dir=$1\n  desc \"make directory $dir\"\n\n  mkdirP() { mkdir -p \"$dir\"; }\n  cmd mkdirP\n}\n```\n\nThis is an interesting construction.  It closely resembles a *closure function*, that is, a\nfunction which is aware of the variables in its enclosing scope.\n\nThat's not what's going on here, although it does in fact behave like a closure because of\nour limited use case.  So long as the call to `mkdirP` is made from within `mkdirTask`, as\nit is here, Bash's dynamic scoping will allow `mkdirP` will see the `dir` belonging to\n`mkdirTask`.  `mkdirP` could even be defined elsewhere, but it is usually more readable to\ndefine it where it is consumed like this.\n\nThe concern this resolves is that, within `mkdirP`, quoting is handled normally.  We aren't\nembedding a command in a string, so it's only evaluated once as you'd expect.\n\nThis is generally the best pattern for parameterized tasks and handles additional complexity\nnicely, since function syntax is friendlier than evaluated string syntax.  For example,\nsyntax highlighting editors don't generally highlight within strings.\n\n## Example\n\nSee `update-env` as an example of what can be accomplished with a configuration script.  It\nis the script I use on my own machines.  Relying on nix and home-manager allows it to\nspecify packages in a dotfiles repository, which saves from having to track them in the\nscript.\n\nIn particular, see the boilerplate at the end for an example of how to make it curl-pipeable\nfrom GitHub.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbinaryphile%2Ftask.bash","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbinaryphile%2Ftask.bash","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbinaryphile%2Ftask.bash/lists"}