An open API service indexing awesome lists of open source software.

https://github.com/shuckster/statebot-sh

Statebot for shell-scripts. Describe the states and allowed transitions of a program using a flowchart-like syntax.
https://github.com/shuckster/statebot-sh

code-organization emitting-events fsm state-machine state-management

Last synced: 3 months ago
JSON representation

Statebot for shell-scripts. Describe the states and allowed transitions of a program using a flowchart-like syntax.

Awesome Lists containing this project

README

          

Statebot-sh

Statebot for shell-scripts. Describe the states and allowed transitions of a program using a flowchart-like syntax.

Statebot-sh is an [FSM](https://en.wikipedia.org/wiki/Finite-state_machine). A minimal, but useful shell port of the version that runs in [Node and the browser](https://shuckster.github.io/statebot/). It employs a simple caching mechanism with `/tmp/statebots.csv` in order to persist the states of your machines across runs, and optionally the events too.

- [Quick Start](#quick-start)
- [A small example](#a-small-example)
- [The API](#the-api)
- [Handling events and transitions](#handling-events-and-transitions)
- [Limitations](#limitations)
- [Why?](#why)
- [Credits](#credits)
- [License](#license)

Documentation is in this README, and examples can be found in `/examples`. It's not required reading, but the original JavaScript version has [extensive documentation](https://shuckster.github.io/statebot/) that may be helpful with getting-to-grips with this port. In particular, familiarity with the [chart syntax](https://zansh.in/statebot/types/index.TStatebotChart.html) would be helpful to know (it's not very complicated.)

# Quick Start

`install.sh` will install Statebot-sh and its examples into `/opt/statebot`:

```sh
# Run install.sh using curl
sh -c "$(curl -fsSL https://raw.githubusercontent.com/shuckster/statebot-sh/master/install.sh)"
```
```sh
# Run install.sh using wget
sh -c "$(wget -qO- https://raw.githubusercontent.com/shuckster/statebot-sh/master/install.sh)"
```

## Manual installation

Of course, you can also just download whatever you like directly from [the repository](https://github.com/shuckster/statebot-sh). Only `statebot.sh` is required:

```sh
curl https://raw.githubusercontent.com/shuckster/statebot-sh/master/statebot.sh > statebot.sh
```

You don't need to make it executable, but if you do, running it will display an example and the API documentation:

```sh
chmod +x statebot.sh
./statebot.sh
```

# A small example:

```sh
#!/bin/sh
STATEBOT_LOG_LEVEL=4
# 0 for silence, 4 for everything

STATEBOT_USE_LOGGER=0
# 1 to use the `logger` command instead of `echo`

#
# Define the states and allowed transitions:
#
PROMISE_CHART='

idle ->
// Behaves a bit like a JS Promise
pending ->
(rejected | resolved) ->
idle

'
#
# Implement "perform_transitions" to act on events:
#
perform_transitions ()
{
local ON THEN
ON=""
THEN=""

case $1 in
'idle->pending')
ON="start"
THEN="hello_world"
;;
'pending->resolved')
ON="okay"
THEN="statebot_emit done"
;;
'rejected->idle'|'resolved->idle')
ON="done"
THEN="all_finished"
;;
esac

echo $ON "$THEN"

# The job of this function is to "echo" the event-
# name that will cause the transition to happen.
#
# Optionally, it can also "echo" a command to run
# after the transition happens.
#
# Following the convention set in the JS version
# of Statebot, this is called a "THEN" command.
# It can be anything you like, including a Statebot
# API call.
#
# It's important to just echo the name of an event
# (and optional command, too) rather than execute
# something directly! Anything that is echo'ed by
# this function that is not an event or command-
# name might result in some wild behaviour.
}

#
# THEN callbacks:
#
hello_world ()
{
echo "Hello, World!"
statebot_emit "okay"
}

all_finished ()
{
echo "Done and done!"
}

#
# Entry point
#
main ()
{
statebot_init "demo" "idle" "start" "$PROMISE_CHART"
# machine name -^ ^ ^ ^
# 1st-run state -------+ | |
# 1st-run event --------------+ |
# statebot chart --------------------------+

echo "Current state: $CURRENT_STATE"
echo "Previous state: $PREVIOUS_STATE"

if [ "$1" = "" ]
then
exit
fi

# Send events/reset signal from the command-line:
if [ "$1" = "reset" ]
then
statebot_reset
else
statebot_emit "$1"
fi
}

cd "${0%/*}" || exit
# (^- change the directory to where this script is)

# Import Statebot-sh
# shellcheck disable=SC1091
. ./statebot.sh

main "$1"
```

This example, along with some more complex ones, is available in the `/examples` folder.

# The API

This port implements a subset of the API in the JavaScript version of Statebot.

You can see API documentation by running `./statebot.sh --api`

Here's the output:

```sh
statebot_inspect "$YOUR_CHART"
#
# When developing your charts, it is useful
# to see the transitions they represent so
# you can copy-paste into your perform/
# on_transitions() functions.
#
# Use statebot_inspect() to give you this
# information, and the states too.

statebot_init "example" "idle" "start" "idle -> done"
# ^ ^ ^ ^
# machine name -| | | |
# 1st-run state --------+ | |
# 1st-run event --------------+ |
# statebot chart ------------------------+
#
# If your machine does not yet have an
# entry in the CSV database, this will
# initialise it with the values passed-in.
#
# If the machine already exists, then the
# values in the DB will be used from this
# point onwards.
#
# Only one machine is allowed per script,
# so do not call this more than once in
# order to try and have multiple state-
# machines in the same script! It is easy
# to use Statebot in many different and
# independent scripts.
#
# (Charts are not stored in the DB, and
# are specified as the last argument
# in order to enforce setting defaults
# for the initial state/event too.)

statebot_emit "start" persist
# ^ ^
# event name --+ |
# |
# Store this event ----+ (optional)
# for a future run instead of calling it
# immediately.
#
# When you run your script again later, the
# event will be picked-up by the call to
# statebot_init().

statebot_enter "pending"
# ^
# state name --+
#
# Changes to the specified state, if allowed
# by the rules in the state-chart.

statebot_reset
#
# Reset the machine to its 1st-run state
# & event. No events will be emitted.

statebot_states_available_from_here
#
# List the states available from the
# current-state.

statebot_current_state_of "statebot_name"
statebot_persisted_event_of "statebot_name"
# ^
# machine name -----------------+
#
# Get the current-state / persisted-event
# of the machine called "statebot_name".

statebot_delete "statebot_name"
# ^
# machine name -----+
#
# Delete the record of a "statebot_name"
# from the database.

# Details about the current machine:
echo " Current state: $CURRENT_STATE"
echo " Previous state: $PREVIOUS_STATE"
echo "Last emitted event: $PREVIOUS_EVENT"
```

## Handling events and transitions

If you want Statebot to do something when you run `statebot_emit()` and `statebot_enter()` other than just changing the `CURRENT_STATE` variable, then you need to define at least one of these functions:

- `perform_transitions()`
- `on_transitions()`

These should be available before calling `statebot_init()`.

`perform_transitions()` has the following signature:

```sh
perform_transitions ()
{
local ON THEN
ON=""
THEN=""

# A string in the form `from->to` will be passed-in
# as the only argument ($1) to this function, and it
# represents a state-transition.
case $1 in
'idle->pending')
ON="start"
THEN="statebot_emit okay persist"
;;
'pending->resolved')
ON="okay"
THEN="statebot_emit done"
;;
'rejected->idle'|'resolved->idle')
ON="done"
;;
esac

echo $ON $THEN

# The job of this function is to "echo" the event-
# name that will cause the transition to happen.
#
# Optionally, it can also "echo" a command to run
# after the transition happens.
#
# Following the convention set in the JS version
# of Statebot, this is called a "THEN" command.
# It can be anything you like, including a Statebot
# API call.
#
# It's important to just echo the name of an event
# (and optional command, too) rather than execute
# something directly! Anything that is echo'ed by
# this function that is not an event or command-
# name might result in some wild behaviour.
}
```

`on_transitions()` is similar:

```sh
on_transitions ()
{
local THEN
THEN=""

# A string in the form `from->to` will be passed-in
# as the only argument ($1) to this function, and it
# represents the state-transition that just happened.
case $1 in
'idle->pending')
THEN="echo Hello, World!"
;;
'rejected->idle'|'resolved->idle')
THEN="all_finished"
;;
esac

echo $THEN

# The job of this function is to "echo" the name of
# a command you want to run.
#
# Again, it's important to just echo the name of
# a command rather than executing it directly!
}
```

Note that `statebot_emit()` will also call `on_transitions()` if an event causes a transition to happen, so long as the function has been defined.

## Limitations

The JavaScript version of Statebot obviously has a bigger API, but it also allows more complex transition-names in its `performTransitions()` and `onTransitions()` hitchers, for example:

```js
machine.onTransitions({
'resolved | rejected -> done': () => {
console.log('All finished')
}
})
```

To emulate this in Statebot-sh, the function `case_statebot` is offered:

```sh
on_transitions ()
{
local THEN
THEN=""

if case_statebot $1 '
resolved | rejected -> done
'
then
THEN="echo 'All finished'"
fi

echo $THEN
}
```

It takes a transition as the first-argument, and a statebot-chart as the second. If the transition exists in the chart, an exit-code of `0` is returned.

Abusing this will manifest as a performance-hit on slow devices, so I recommend using it only in a wildcard `*)` at the end of a regular case-statement.

For example:

```sh
perform_transitions ()
{
local ON THEN
ON=""
THEN=""

case $1 in
# Handle your "simple" transitions first:
'idle->pending')
ON="start"
THEN="statebot_emit okay"
;;

*)
# Now in the wildcard section, use
# case_statebot() for your complex
# rules:
if case_statebot $1 '
rejected | resolved -> idle
'
then
ON="done"
fi
;;
esac

echo $ON $THEN
}
```

# Why?

After writing the [JavaScript version of Statebot](https://shuckster.github.io/statebot/) I really wanted the same kind of API to help me write more predictable scripts for my little hobby devices.

I know **Raspberry Pi** is all the rage, so I could have just installed Node on one of those and used the JS version of Statebot. But I also have some considerably less beefy embedded devices that can't run Node, so I really liked the idea of getting Statebot running as a shell-script.

Needless to say, it now satisfies my own needs enough that I'm happy to share it. :)

I hope you find it useful!

# Contributing

My main goal with this version is to make it as portable as possible! If you find an issue with it working in your particular version of `sh` or `bash`, please file a bug!

If you feel it has saved you a little pain while trying to grok your own old projects, please consider [buying me a coffee](https://www.buymeacoffee.com/shuckster). :)

## Credits

Statebot was inspired by a trawl through Wikipedia and Google, which in turn was inspired by [XState](https://github.com/davidkpiano/xstate) by David Khourshid. You should check it out.

The Statebot logo uses the "You're Gone" font from [Typodermic Fonts](https://typodermicfonts.com/youre-gone/). The logo was made with [Acorn](https://flyingmeat.com/acorn/). The JS documentation is written in [JSDoc](https://jsdoc.app/) and is built with [Typedoc](https://typedoc.org/).

Statebot-sh was written by [Conan Theobald](https://github.com/shuckster/).

## License

Statebot-sh is [MIT licensed](./LICENSE).