Coder Social home page Coder Social logo

taskfile's Introduction

Taskfile

This repository contains the default Taskfile template for getting started in your own projects. A Taskfile is a bash (or zsh etc.) script that follows a specific format. It's called Taskfile, sits in the root of your project (alongside your package.json) and contains the tasks to build your project.

#!/bin/bash
PATH=./node_modules/.bin:$PATH

function install {
    npm install
}

function build {
    webpack
}

function start {
    build # Call task dependency
    python -m SimpleHTTPServer 9000
}

function test {
    mocha test/**/*.js
}

function default {
    # Default task to execute
    start
}

function help {
    echo "$0 <task> <args>"
    echo "Tasks:"
    compgen -A function | cat -n
}

TIMEFORMAT="Task completed in %3lR"
time ${@:-default}

And to run a task:

$ run build
Hash: 31b6167c7c8f2920e0d2
Version: webpack 2.1.0-beta.25
Time: 4664ms
   Asset     Size  Chunks             Chunk Names
index.js  1.96 MB       0  [emitted]  index
    + 353 hidden modules
Task completed in 0m5.008s

Install

To "install", add the following to your .bashrc or .zshrc (or .whateverrc):

# Quick start with the default Taskfile template
alias run-init="curl -so Taskfile https://raw.githubusercontent.com/adriancooney/Taskfile/master/Taskfile.template && chmod +x Taskfile"

# Run your tasks like: run <task>
alias run=./Taskfile

Usage

Open your directory and run run-init to add the default Taskfile template to your project directory:

$ cd my-project
$ run-init

Open the Taskfile and add your tasks. To run tasks, use run:

$ run help
./Taskfile <task> <args>
Tasks:
     1  build
     2  build-all
     3  help
Task completed in 0m0.005s

Techniques

Arguments

Let’s pass some arguments to a task. Arguments are accessible to the task via the $1, $2, $n.. variables. Let’s allow us to specify the port of the HTTP server:

#!/bin/bash

function serve {
  python -m SimpleHTTPServer $1
}

"$@"

And if we run the serve task with a new port:

$ ./Taskfile serve 9090
Serving HTTP on 0.0.0.0 port 9090 ...

Using npm Packages

One of the most powerful things about npm run-scripts (who am I kidding, it’s definitely the most powerful thing) is the ability to use the CLI interfaces for many of the popular packages on npm such as babel or webpack. The way npm achieves this is by extending the search PATH for binaries to include ./node_modules/.bin. We can do this to very easily too by extending the PATH at the top of our Taskfile to include this directory. This will enable us to use our favourite binaries just like we would in an npm run-script:

#!/bin/bash
PATH=./node_modules/.bin:$PATH

function serve {
  python -m SimpleHTTPServer $1
}

function build {
  webpack src/index.js --output-path build/
}

function lint {
  eslint src
}

function test {
  mocha src/**/*.js
}

"$@"

Task Dependencies

Sometimes tasks depend on other tasks to be completed before they can start. To add another task as a dependency, simply call the task's function at the top of the dependant task's function.

#!/bin/bash
PATH=./node_modules/.bin:$PATH

function clean {
  rm -r build dist
}

function build {
  webpack src/index.js --output-path build/
}

function minify {
  uglify build/*.js dist/
}

function deploy {
  clean && build && minify
  scp dist/index.js [email protected]:/top-secret/index.js
}

"$@"

Parallelisation

To run tasks in parallel, you can us Bash’s & operator in conjunction with wait. The following will build the two tasks at the same time and wait until they’re completed before exiting.

#!/bin/bash
PATH=./node_modules/.bin:$PATH

function build {
    echo "beep $1 boop"
    sleep 1
    echo "built $1"
}

function build-all {
    build web & build mobile &
    wait
}

"$@"

And execute the build-all task:

$ run build-all
beep web boop
beep mobile boop
built web
built mobile

Default task

To make a task the default task called when no arguments are passed, we can use bash’s default variable substitution ${VARNAME:-<default value>} to return default if $@ is empty.

#!/bin/bash
PATH=./node_modules/.bin:$PATH

function build {
    echo "beep boop built"
}

function default {
    build
}

"${@:-default}"

Now when we run ./Taskfile, the default function is called.

Runtime Statistics

To add some nice runtime statistics like Gulp so you can keep an eye on build times, we use the built in time and pass if a formatter.

#!/bin/bash
PATH=./node_modules/.bin:$PATH

function build {
    echo "beep boop built"
    sleep 1
}

function default {
    build
}

TIMEFORMAT="Task completed in %3lR"
time ${@:-default}

And if we execute the build task:

$ ./Taskfile build 
beep boop built 
Task completed in 0m1.008s

Help

The final addition I recommend adding to your base Taskfile is the task which emulates, in a much more basic fashion, (with no arguments). It prints out usage and the available tasks in the Taskfile to show us what tasks we have available to ourself.

The compgen -A function is a bash builtin that will list the functions in our Taskfile (i.e. tasks). This is what it looks like when we run the task:

$ ./Taskfile help
./Taskfile <task> <args>
Tasks:
     1  build
     2  default
     3  help
Task completed in 0m0.005s

task: namespace

If you find you need to breakout some code into reusable functions that aren't tasks by themselves and don't want them cluttering your help output, you can introduce a namespace to your task functions. Bash is pretty lenient with it's function names so you could, for example, prefix a task function with task:. Just remember to use that namespace when you're calling other tasks and in your task:$@ entrypoint!

#!/bin/bash
PATH=./node_modules/.bin

function task:build-web {
    build-target web
}

function task:build-desktop {
    build-target desktop
}

function build-target {
    BUILD_TARGET=$1 webpack --production
}

function task:default {
    task:help
}

function task:help {
    echo "$0 <task> <args>"
    echo "Tasks:"

    # We pick out the `task:*` functions
    compgen -A function | sed -En 's/task:(.*)/\1/p' | cat -n
}

TIMEFORMAT="Task completed in %3lR"
time "task:${@:-default}"

Executing tasks

So typing out ./Taskfile every time you want to run a task is a little lousy. just flows through the keyboard so naturally that I wanted something better. The solution for less keystrokes was dead simple: add an alias for run (or task, whatever you fancy) and stick it in your .zshrc. Now, it now looks the part.

$ alias run=./Taskfile
$ run build
beep boop built
Task completed in 0m1.008s

Quickstart

Alongside my run alias, I also added a run-init to my .zshrc to quickly get started with a new Taskfile in a project. It downloads a small Taskfile template to the current directory and makes it executable:

$ alias run-init="curl -so Taskfile https://medium.com/r/?url=https%3A%2F%2Fraw.githubusercontent.com%2Fadriancooney%2FTaskfile%2Fmaster%2FTaskfile.template && chmod +x Taskfile"

$ run-init
$ run build
beep boop built
Task completed in 0m1.008s

Importing from npm

If you've the incredible jq installed (you should, it's so useful), here's a handy oneliner to import your scripts from your package.json into a fresh Taskfile. Copy and paste this into your terminal with your package.json in the working directory:

run-init && (head -n 3 Taskfile && jq -r '.scripts | to_entries[] | "function \(.["key"]) {\n    \(.["value"])\n}\n"' package.json | sed -E 's/npm run ([a-z\:A-Z]+)/\1/g' && tail -n 8 Taskfile) > Taskfile.sh && mv Taskfile.sh Taskfile && chmod +x Taskfile 

And the importer explained:

$ run-init && \ # Download a fresh Taskfile template
    (
        head -n 3 Taskfile && \ # Take the Taskfile template header
        # Extract the scripts using JQ and create bash task functions
        jq -r '.scripts | to_entries[] | "function \(.["key"]) {\n    \(.["value"])\n}\n"' package.json \ 
            | sed -E 's/npm run ([a-z\:A-Z]+)/\1/g' \ # Replace any `npm run <task>` with the task name
        && tail -n 8 Taskfile # Grab the Taskfile template footer
    ) \ # Combine header, body and footer
    > Taskfile.sh && mv Taskfile.sh Taskfile && chmod +x Taskfile # Pipe out to Taskfile

To fix up your npm run-scripts to use the Taskfile, you can also use JQ to do this automatically for you:

jq '.scripts = (.scripts | to_entries | map(.value = "./Taskfile \(.key)") | from_entries)' package.json > package.json.2 && mv package.json.2 package.json

Free Features

  • Conditions and loops. Bash and friends have support for conditions and loops so you can error if parameters aren’t passed or if your build fails.
  • Streaming and piping. Don’t forget, we’re in a shell and you can use all your favourite redirections and piping techniques.
  • All your standard tools like rm and mkdir.
  • Globbing. Shells like zsh can expand globs like **/*.js for you automatically to pass to your tools.
  • Environment variables like NODE_ENV are easily accessible in your Taskfiles.

Considerations

When writing my Taskfile, these are some considerations I found useful:

  • You should try to use tools that you know users will have installed and working on their system. I’m not saying you have to be POSIX.1 compliant but be weary of using tools that aren’t standard (or difficult to install).
  • Keep it pretty. The reason for the Taskfile format is to keep your tasks organised and readable.
  • Don’t completely ditch the package.json. You should proxy the scripts to the Taskfile by calling the Taskfile directory in your package.json like "test": "./Taskfile test". You can still pass arguments to your scripts with the -- special argument and npm run build -- --production if necessary.

Caveats

The only caveat with the Taskfile format is we forgo compatibility with Windows which sucks. Of course, users can install Cygwin but one of most attractive things about the Taskfile format is not having to install external software to run the tasks. Hopefully, [Microsoft’s native bash shell in Windows 10](http://www.howtogeek.com/249966 how-to-install-and-use-the-linux-bash-shell-on-windows-10/) can do work well for us in the future.


Collaboration

The Taskfile format is something I’d love to see become more widespread and it’d be awesome if we could all come together on a standard of sorts. Things like simple syntax highlighting extensions or best practices guide would be awesome to formalise.

taskfile's People

Contributors

adriancooney avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

taskfile's Issues

Some suggestions

Hello, I have been using this for quite a while and have added some of my own flare to it. I figured I would drop it off and see if you are interested.

Here is an example gist I made for controlling a docker compose infrastructure: https://gist.github.com/tvalladon/e316bee4b58ca082d2190be023565949

The updated help command will check for ## Some Description on the function line and display those descriptions inline (taken and modified from a self documenting Makefile).

./Taskfile <task> <args>
control_example                Test different control pathways <start | stop | restart>
control_example_with_optional  Test different control pathways <start | stop | restart> [service name]
debug_example                  This is an example of using the debug command
help                           Display usage for this application
inline_debug_example           While working on a command debug is useful
no_arg                         No args required
one_arg                        One arg required
two_arg                        Two args required
Task completed in 0m0.002s

The debug can help when coming up with the proper settings or commands to complete the task, for example piping to tr, grep, xarg or checking bash regex in commands like FILENAME=src/test1.txt;cat ${FILENAME} > ${FILENAME/.txt/.bak}.

Let me know if there is anything I can do to help. I tried to toss in some general examples of how I am using the system right now.

I also set the commands to return an exit code if the required args are missing so you can properly chain on success, such as run one arg && run two && run three arg, the run three would not execute if run two required an arg that was missing.

#!/bin/bash
PATH=./node_modules/.bin:$PATH

function debug {
    echo "Stopped in REPL. Press ^D to resume, or ^C to abort."
    local line
    while read -r -p "> " line; do
        eval "$line"
    done
    echo
}

function no_arg { ## No args required
    echo $*
}

function one_arg { ## One arg required
    if [ $# -ne 1 ]; then echo 1>&2 "Usage: $0 $FUNCNAME <arg>";exit 3;fi
    echo $*
}

function two_args { ## Two args required
    if [ $# -ne 2 ]; then echo 1>&2 "Usage: $0 $FUNCNAME <arg> <arg>";exit 3;fi
    echo $*
}

function debug_example { ## This is an example of using the debug command
    debug
}

function control_example { ## Test different control pathways <start | stop | restart>
    if [ $# -ne 1 ]; then echo 1>&2 "Usage: $0 $FUNCNAME <start | stop | restart>";exit 3;fi

    if [ $1 = "start" ]; then
        echo "Starting..."
        echo $*
    fi

    if [ $1 = "stop" ]; then
        echo "Stoping...."
        echo $*
    fi

    if [ $1 = "restart" ]; then
        echo "Restarting..."
        echo $*
        control_example stop && control_example start
    fi
}

function control_example_with_optional { ## Test different control pathways <start | stop | restart> [service name]
    if [ $# -lt 1 ]; then echo 1>&2 "Usage: $0 $FUNCNAME <start | stop | restart> [service name]";exit 3;fi

    if [ $1 = "start" ]; then
        echo "Starting ${2}..."
        echo $*
    fi

    if [ $1 = "stop" ]; then
        echo "Stoping ${2}..."
        echo $*
    fi

    if [ $1 = "restart" ]; then
        echo "Restarting ${2}..."
        echo $*
        control_example_with_optional stop $2 && control_example_with_optional start $2
    fi
}

function inline_debug_example { ## While working on a command debug is useful
    VALUE="testing"
    cd src
    ls
    ls -alth
    echo "if you type \`echo \$VALUE\` in the following REPL you will see the set value of ${VALUE}"
    debug
}

function default {
    help
}

function help { ## Display usage for this application
    echo "$0 <task> <args>"
    grep -E '^function [a-zA-Z_-]+ {.*?## .*$$' $0 | sed -e 's/function //' | sort | awk 'BEGIN {FS = "{.*?## "}; {printf "\033[93m%-30s\033[92m %s\033[0m\n", $1, $2}'
}

TIMEFORMAT="Task completed in %3lR"
time ${@:-default}```

Still active?

I realized I am abusing Makefiles, and am searching for an alternative, but issues and PRs here have not been answered for years?

Ability to define functions that aren't exposed as tasks

Hey, great idea btw. Really enjoying this pattern.

But I ran into a situation where I would very much like to add a function into my Taskfile but not have it be runnable directly, or perhaps as a compromise not be listed in the help menu.

This comes in handy when you want to define a "helper" function that multiple tasks can call.

For example, currently I have some tasks that look like this:

function flask {
  ## Run any Flask commands
  docker-compose exec web flask "${@}"
}

function flake8 {
  ## Lint Python code with flake8
  docker-compose exec web flake8 "${@}"
}

It would be really nice if I could do this instead:

function _web {
  docker-compose exec web "${@}"
}

function flask {
  ## Run any Flask commands
  _web flask "${@}"
}

function flake8 {
  ## Lint Python code with flake8
  _web flake8 "${@}"
}

And then the help menu would only show the flask and flake8 commands and running _web directly would throw a command not found.

I figured we could mark these helper functions with an underscore to distinguish what should be private or not.

Method I've tried so far that partially works

You can do compgen -A function | grep -v "^_" | cat -n which hides them from the task list but technically you can still run the helper functions directly.

Any thoughts on how to do this in a better way?

slightly more helpful help, if it's helpful

I know the idea is to be lightweight as possible, but here is a slightly more helpful help, if it's helpful for anybody.

It omits the help and default tasks from the list of commands, and then displays the contents of the default task:

function help {
  echo "$0 <task> <args>"
  echo "Tasks:"
  compgen -A function | grep -v 'help\|default' | cat -n
  echo
  type default | sed -e '1d; 2s/()/task:/; s/^/    /'
  echo
}

The output looks like this, for an example Taskfile with a slightly complicated default for illustration:

./Taskfile <task> <args>
Tasks:
     1	install
     2	build
     3	run
     4	update
     5	upgrade

    default task: 
    { 
        update;
        for file in src/*;
        do
            upgrade $file;
        done
    }

Task completed in 0m0.003s

this is the neatest thing !

at first, I thought this was just the commands and that there is a script somewhere to run these commands ... then I realized the "script" is just the final line at the end

time ${@:-default}

bam, so neat

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.