diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..da8d73b --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,35 @@ +name: Test + +on: + push: + branches: [ '*' ] + tags: [ 'v*' ] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Run ShellCheck + uses: ludeeus/action-shellcheck@master + env: + SHELLCHECK_OPTS: -x + test: + runs-on: ubuntu-latest + steps: + - name: Setup BATS + run: | + git clone --depth 1 --branch v1.10.0 https://github.com/bats-core/bats-core.git /tmp/bats + sudo /tmp/bats/install.sh /usr/local + rm -rf /tmp/bats + sudo mkdir -p /usr/lib/bats/bats-support /usr/lib/bats/bats-assert + wget -O- 'https://github.com/ztombol/bats-support/archive/refs/tags/v0.3.0.tar.gz' | \ + sudo tar -xz --strip-components=1 -C /usr/lib/bats/bats-support + wget -O- 'https://github.com/ztombol/bats-assert/archive/refs/tags/v0.3.0.tar.gz' | \ + sudo tar -xz --strip-components=1 -C /usr/lib/bats/bats-assert + - name: Checkout + uses: actions/checkout@v3 + - uses: orbit-online/upkg@v0.14.0 + - name: Run tests + run: bats . diff --git a/README.md b/README.md new file mode 100644 index 0000000..ce417cc --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# trap.sh + +Library for managing bash signal traps. + +## Contents + +- [Installation](#installation) +- [Usage](#usage) + +## Installation + +With [μpkg](https://github.com/orbit-online/upkg) + +``` +upkg install -g orbit-online/trap.sh@ +``` + +## Usage + +### `trap_append SIGNAL CMD` + +Appends `CMD` to the `SIGNAL` trap. `$TRAP_POINTER` will contain a pointer +that can be used with `trap_remove` to remove the command again. + +### `trap_remove POINTER` + +Removes the command identified by `POINTER` from the list of traps to run. diff --git a/test.bats b/test.bats new file mode 100644 index 0000000..ae7ca32 --- /dev/null +++ b/test.bats @@ -0,0 +1,34 @@ +#!/usr/bin/env bats +# shellcheck disable=2030,2031 + +load '/usr/lib/bats/bats-support/load' +load '/usr/lib/bats/bats-assert/load' + +setup_file() { + bats_require_minimum_version 1.5.0 +} + +@test 'trap is run on signal' { + assert_equal "$(bash -ec "source $BATS_TEST_DIRNAME/trap.sh + trap_append USR2 \"echo 'USR2 signal received'\" + kill -s USR2 \$\$ + ")" 'USR2 signal received' +} + +@test 'trap removal works' { + assert_equal "$(bash -ec "source $BATS_TEST_DIRNAME/trap.sh + trap_append USR2 \"echo 'cmd to remove'\" + p=\$TRAP_POINTER + trap_append USR2 \"echo 'USR2 signal received'\" + trap_remove \$p + kill -s USR2 \$\$ + ")" 'USR2 signal received' +} + +@test 'only commands for specific signal are run' { + assert_equal "$(bash -ec "source $BATS_TEST_DIRNAME/trap.sh + trap_append USR1 \"echo 'USR1 signal received'\" + trap_append USR2 \"echo 'USR2 signal received'\" + kill -s USR2 \$\$ + ")" 'USR2 signal received' +} diff --git a/trap.sh b/trap.sh new file mode 100644 index 0000000..35e3434 --- /dev/null +++ b/trap.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +TRAP_CMDS=() + +_trap_run() { + local signal=$1 trap_cmd + for trap_cmd in "${TRAP_CMDS[@]}"; do + [[ $trap_cmd = $signal* ]] || continue + eval "${trap_cmd#"$signal "}" || true + done +} + +trap_append() { + local signal=$1 cmd=$2 trap_cmd + TRAP_POINTER=0 + for trap_cmd in "${TRAP_CMDS[@]}"; do + [[ -n $trap_cmd ]] || break + : $((TRAP_POINTER++)) + done + TRAP_CMDS[TRAP_POINTER]="$signal $cmd" + # shellcheck disable=SC2064 + [[ $(trap -p "$signal") = "trap -- '_trap_run $signal' $signal" ]] || trap "_trap_run $signal" "$signal" +} + +trap_remove() { + local pointer=$1 + TRAP_CMDS[pointer]='' +} diff --git a/upkg.json b/upkg.json new file mode 100644 index 0000000..231a948 --- /dev/null +++ b/upkg.json @@ -0,0 +1,3 @@ +{ + "assets": ["trap.sh"] +}