Skip to main content

About fn

fn is a function-oriented general purpose automation tool that aims to be simpler and more flexible than similar tools such as Make, Task, and Rake.

fn is a commandline program that runs functions (shortened to fns) defined in a fnfile.yml, similar to how you use make to run targets in a Makefile.

Design Philosophy

fn aims to have a human-centered design, with emphasis on usability and aesthetics.

Even the smartest among us can feel inept as we fail to figure out which light switch or oven burner to turn on, or whether to push, pull, or slide a door. The fault lies not in ourselves, but in product design that ignores the needs of users and the principles of cognitive psychology.

The problems range from ambiguous and hidden controls to arbitrary relationships between controls and functions, coupled with a lack of feedback or other assistance and unreasonable demands on memorization.

The rules are simple: make things visible, exploit natural relationships that couple function and control, and make intelligent use of constraints.

The goal: guide the user effortlessly to the right action on the right control at the right time.

-- The Design of Everyday Things

What is a function?

Functions are "self-contained" modules of code that accomplish a specific task. Functions usually "take in" data, process it, and "return" a result. Once a function is written, it can be used over and over and over again. Functions can be "called" from the inside of other functions.

The very basic concept of "taking in" data and "returning a result" is lost by other tools, and indeed, I found myself constantly wanting to do this exact thing all the time.

Basics

An example to get you started:

fnfile.yml
version: '0.1'
fns:
hello: echo "Hello, World!"
❯ fn hello
Hello, World

Understanding the fnfile

The top-level fns is a map of defined functions. With the key being the name of the fn, and the value being one of several types, allowing you to express intent without requiring excessive syntax.

fn - Function Definition

The common fields of an fn are:

  • do - The do step. required**
  • dir - The working directory when running shell steps. The default is same directory as the fnfile.yml.

Common Steps

do

Do performs the given steps serially.

do:
steps: [ <STEP> ]

parallel

Parallel performs the given steps in parallel.

parallel:
steps: [ <STEP> ]

sh

Sh runs a shell command.

sh:
run: <string>

Flexible Nature

The following tasks are all the same. The resolve to a single sh step, which runs a shell command.

fns:
hello: echo "hello"
foo:
- echo "foo"
bar:
do: echo "bar"
baz:
do:
- echo "baz"
biz:
do:
- sh: echo "biz"
qux:
do:
steps:
- echo "qux"
ipsum:
do:
steps:
- sh: echo "ipsum"
amet:
do:
steps:
- sh:
- run: echo "amet"

Why is this important?

The design of fn is to get out of your way as quickly as possible. Meaning if you don't need certain functionality, you don't need to declare it or even make room for it.

Let's take a fn as an example of how we can some liberties without affecting functionality, requiring you to write a bunch of code, or affecting readability.

The definition of a fn is that of a function definition, declaring among other things, what steps should take place. fn gives you a do as your "entrypoint" to declaring those steps.

fns:
my-function:
do:
steps:
- sh:
run: echo "hello"

Here we are declaring a do with 1 step, that of an sh step when runs the shell command: echo "hello".

The first shortcut, is that for sh.

The definition of a sh step:

# VALID
sh:
run: <string>
dir: <string, optional>

Given that run is the only required field, you can shortcut this by simply using:

sh: <string>

Furthermore, since sh is very likely the most common thing you will likely be writing (since it is the step type that actually executes work), in many cases, we can shortcut this entirely to just:

<string>

So the first step is to reduce sh to just sh: <string>

  fns:
my-function:
do:
steps:
+ - sh: echo "hello"
- - sh:
- run: echo "hello"

Furthermore, fn makes an assumption, that if it sees just a string in a place that would accept a step, that string is assumed to be a sh step:

  fns:
my-function:
do:
steps:
+ - echo "hello"
- - sh: echo "hello"

But there's a few more optimizations to make. For one, you aren't using any other functionality of do, just the steps field. The steps field is also the most important part of a do step. So we can shortcut the entire do into just an array, and not require you explicitly declare steps:

  fns:
my-function:
do:
+ - echo "hello"
- steps:
- - echo "hello"

Furthermore, if you have only 1 step, why should I force you to use an array?

  fns:
my-function:
+ do: echo "hello"
- do:
- - echo "hello"

We can apply the same logic one more time, to declare an entire fn definition as that of a single sh step, with the following:

  fns:
+ my-function: echo "hello"
- my-function:
- do: echo "hello"

So for the simplest of things, there's really not much to write.