diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0b37f2b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,78 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ${{ matrix.os }} + container: ${{ matrix.container }} + name: ${{ matrix.label }} + defaults: + run: + shell: bash + # Mirror alexzhangs/xsh's shell matrix: bash 3.2 / 4.4 / 5.x + zsh 5.x. + # Utilities run under zsh's ksh emulation (provided by xsh >= 0.7.0). + strategy: + fail-fast: false + matrix: + include: + - { os: ubuntu-latest, container: '', shell_path: /bin/bash, label: bash-5.x-linux } + - { os: macos-latest, container: '', shell_path: /bin/bash, label: bash-3.2-macos } + - { os: macos-latest, container: '', shell_path: brew, label: bash-5.x-macos } + - { os: ubuntu-latest, container: rockylinux:8, shell_path: /bin/bash, label: bash-4.4-linux } + - { os: macos-latest, container: '', shell_path: /bin/zsh, label: zsh-5.x-macos } + - { os: ubuntu-latest, container: '', shell_path: /usr/bin/zsh, label: zsh-5.x-linux } + + steps: + - name: Pre-install git (rockylinux container) + if: matrix.container != '' + run: dnf install -y git + + - name: Install dependencies (rockylinux container) + if: matrix.container != '' + run: | + dnf install -y --allowerasing coreutils + dnf install -y gawk sed file findutils diffutils procps-ng which tar gzip python3 + command -v python >/dev/null 2>&1 || ln -sf "$(command -v python3)" /usr/local/bin/python + git config --global --add safe.directory '*' + + - name: Install zsh (Linux) + if: matrix.container == '' && runner.os == 'Linux' && contains(matrix.shell_path, 'zsh') + run: sudo apt-get update && sudo apt-get install -y zsh + + - name: Install Homebrew bash (macOS bash 5.x) + if: matrix.shell_path == 'brew' + run: | + brew install bash + echo "SHELL_PATH=$(brew --prefix)/bin/bash" >> "$GITHUB_ENV" + + - name: Set shell path + if: matrix.shell_path != 'brew' + run: echo "SHELL_PATH=${{ matrix.shell_path }}" >> "$GITHUB_ENV" + + - name: Install xsh + run: | + git clone --depth=50 --branch=master https://github.com/alexzhangs/xsh.git /tmp/xsh + bash /tmp/xsh/install.sh + + - name: Load dependency library xsh-lib/core + run: | + source ~/.xshrc + xsh load xsh-lib/core + + - name: Load library from current branch + run: | + source ~/.xshrc + # Fork PRs fall back to the default branch (the source branch isn't on + # the main repo). + xsh load -b "${{ github.head_ref || github.ref_name }}" xsh-lib/xsql || xsh load xsh-lib/xsql + + - name: Run tests (${{ matrix.label }}) + # test.sh self-sources ~/.xshrc, so running it under $SHELL_PATH makes + # the utilities execute under that shell (bash, or zsh's ksh emulation). + run: | + "$SHELL_PATH" ~/.xsh/repo/xsh-lib/xsql/test.sh diff --git a/.gitignore b/.gitignore index 9f11b75..e69de29 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +0,0 @@ -.idea/ diff --git a/README.md b/README.md index d387400..1447bf5 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,12 @@ +[![GitHub tag](https://img.shields.io/github/v/tag/xsh-lib/xsql?sort=date)](https://github.com/xsh-lib/xsql/tags) +[![GitHub](https://img.shields.io/github/license/xsh-lib/xsql.svg?style=flat-square)](https://github.com/xsh-lib/xsql/) +[![GitHub last commit](https://img.shields.io/github/last-commit/xsh-lib/xsql.svg?style=flat-square)](https://github.com/xsh-lib/xsql/commits/main) + +[![CI](https://github.com/xsh-lib/xsql/actions/workflows/ci.yml/badge.svg)](https://github.com/xsh-lib/xsql/actions/workflows/ci.yml) +[![CodeFactor](https://www.codefactor.io/repository/github/xsh-lib/xsql/badge)](https://www.codefactor.io/repository/github/xsh-lib/xsql) +[![GitHub issues](https://img.shields.io/github/issues/xsh-lib/xsql.svg?style=flat-square)](https://github.com/xsh-lib/xsql/issues) +[![GitHub pull requests](https://img.shields.io/github/issues-pr/xsh-lib/xsql.svg?style=flat-square)](https://github.com/xsh-lib/xsql/pulls) + # xsh-lib/xsql xsh Library - xsh SQL, a pseudo SQL interpreter for Bash.. @@ -6,11 +15,18 @@ About xsh and its libraries, check out [xsh document](https://github.com/alexzha ## Requirements -1. bash +`xsh-lib/xsql` is tested in CI ([GitHub Actions](https://github.com/xsh-lib/xsql/actions/workflows/ci.yml)) on every push and pull request, across the following shell/OS combinations: + +| Shell | Version | OS | Tested | +|-------|---------|-----------------------|:------:| +| bash | 3.2 | macOS | ✅ | +| bash | 4.4 | Linux (rockylinux:8) | ✅ | +| bash | 5.x | Linux (ubuntu-latest) | ✅ | +| bash | 5.x | macOS (Homebrew) | ✅ | +| zsh | 5.x | Linux (ubuntu-latest) | ✅ | +| zsh | 5.x | macOS | ✅ | - Tested with bash: - * 4.3.48 on Linux - * 3.2.57 on macOS +zsh utilities run under xsh's ksh emulation and require **xsh ≥ 0.7.0**. ## Dependency diff --git a/functions/query.sh b/functions/query.sh index fe1603d..1bffa9d 100644 --- a/functions/query.sh +++ b/functions/query.sh @@ -279,7 +279,10 @@ function query () { printf "%s" "$XSQL_QUERY_OFS" fi declare varname="XSQL_QUERY_FIELDS_${qf_name}_ROWS[$row_index]" - printf "%s" "${!varname}" + # `${!varname}` array-element indirection is bash-only; `eval` does + # it portably (the name is built from controlled prefixes + parsed + # field names / numeric indices) + eval "printf '%s' \"\${${varname}}\"" i=$((i + 1)) done echo diff --git a/functions/query/parser.sh b/functions/query/parser.sh index 2b2357e..93b1d12 100644 --- a/functions/query/parser.sh +++ b/functions/query/parser.sh @@ -46,8 +46,12 @@ function parser () { *) case $clause in 'SELECT') - # parse the field list into array - IFS=$', ' read -r -a fields <<< "$1" + # parse the comma/space-separated field list into the + # array. `read -a` is bash-only (zsh's read has no -a); + # field names contain no spaces, so replacing commas + # with spaces and word-splitting is portable. + # shellcheck disable=SC2206 + fields=( ${1//,/ } ) XSQL_QUERY_SELECTED_FIELDS+=( "${fields[@]}" ) ;; 'FROM') diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..652dbd5 --- /dev/null +++ b/test.sh @@ -0,0 +1,79 @@ +#!/bin/bash +# +# Tests for xsh-lib/xsql. Plain assertions, no external test framework. +# +# Usage: +# xsh load xsh-lib/core xsh-lib/xsql # one-time +# bash test.sh # or: zsh test.sh +# + +# Make the `xsh` function available when run as a child process. Under bash it +# is inherited as an exported function (no-op here); zsh cannot export functions, +# so a child `zsh test.sh` sources ~/.xshrc to define xsh as a real zsh function. +if ! type xsh 2>/dev/null | grep -q 'function'; then + # shellcheck source=/dev/null + . ~/.xshrc +fi + +# No `set -e`: query ends in a getopts/return path that can yield a non-zero +# status on success, which zsh's ERR_EXIT would trip inside `$()`. Tally +# failures explicitly instead. Array assertions use `[*]` (joins regardless of +# index base) so they're identical under bash (0-indexed) and zsh (1-indexed). +__fails=0 +assert_eq () { # + if [ "$2" = "$3" ]; then printf 'ok - %s\n' "$1" + else printf 'FAIL - %s: expected [%s], got [%s]\n' "$1" "$2" "$3" >&2; __fails=$((__fails + 1)); fi +} +assert_rc_nonzero () { # + if "${@:2}" >/dev/null 2>&1; then + printf 'FAIL - %s: expected non-zero exit\n' "$1" >&2; __fails=$((__fails + 1)) + else printf 'ok - %s\n' "$1"; fi +} + +xsh log info "xsh list xsql/" +xsh list 'xsql/*' >/dev/null + +xsh log info "import-smoke: all xsql function utilities" +while read -r __lpue; do + [ -z "$__lpue" ] && continue + xsh import "$__lpue" || { printf 'FAIL - import %s\n' "$__lpue" >&2; __fails=$((__fails + 1)); } +done < <(xsh list 'xsql/*' | awk '$1 == "[functions]" {print $2}') +printf 'ok - import-smoke\n' + +# ---------------------------------------------------------------------------- +# xsql/query/parser — parses a SQL expression into XSQL_QUERY_* globals +# (exercises the comma/space field-list split ported off bash-only `read -a`) +# ---------------------------------------------------------------------------- +xsh log info "xsql/query/parser" +xsh xsql/query/parser select f1,f2 from A where f1 = x +assert_eq "parser TABLE" "A" "${XSQL_QUERY_TABLE}" +assert_eq "parser FIELDS" "f1 f2" "${XSQL_QUERY_SELECTED_FIELDS[*]}" +assert_eq "parser WHERE" "f1 = x" "${XSQL_QUERY_WHERE[*]}" +assert_rc_nonzero "parser errors without a FROM table" xsh xsql/query/parser select f1 + +# ---------------------------------------------------------------------------- +# xsql/query — query a text-file "table" (exercises the ${!varname} array +# indirection ported to portable eval) +# ---------------------------------------------------------------------------- +xsh log info "xsql/query" +__tbl=$(mktemp "${TMPDIR:-/tmp}/xsh-xsql-test.XXXXXXXX") +printf 'id name age\n1 alice 30\n2 bob 25\n' > "$__tbl" + +assert_eq "query select+where (-O ,)" "name,age +bob,25" "$(xsh xsql/query -H -O , select name,age from "$__tbl" where id = 2 2>/dev/null || true)" + +assert_eq "query select all rows (-O ,)" "alice +bob" "$(xsh xsql/query -O , select name from "$__tbl" 2>/dev/null || true)" + +assert_eq "query multi-field row order" "bob,25" \ + "$(xsh xsql/query -O , select name,age from "$__tbl" where name = bob 2>/dev/null || true)" +rm -f "$__tbl" + +echo +if [ "$__fails" -eq 0 ]; then + xsh log info "xsql tests: all passed" + exit 0 +else + xsh log error "xsql tests: ${__fails} failure(s)" + exit 1 +fi