shellspec/shellspec

View on GitHub
lib/libexec/translator.sh

Summary

Maintainability
Test Coverage
A
100%
#shellcheck shell=sh disable=SC2004,SC2034,SC2119,SC2120,SC2016

# shellcheck source=lib/libexec.sh
. "${SHELLSPEC_LIB:-./lib}/libexec.sh"
use constants trim match_pattern ends_with escape_quote replace_all match
load grammar

initialize() {
  block_id='' block_id_increased=1 inside_of_example='' inside_of_text=''
  lineno=0 block_no=0 example_no=1 skip_id=0 error='' focused='' skipped=''
  _block_no=0 _block_no_stack='' mock_no=1 inside_of_mock='' use_dsl_in_mock=''
  parameter_count='' parameter_no=0 _parameter_count_stack=''
  parameters_need_example=''
}

finalize() {
  if [ "$_block_no_stack" ]; then
    syntax_error "Unexpected end of file (expecting 'End')"
    lineno=
    while [ "$_block_no_stack" ]; do block_end; done
  fi
  if [ ! "$block_id_increased" ]; then
    trans after_last_block ""
  fi
  trans after_block ""
}

read_specfile() {
  eval "{ IFS= read -r $1 || [ \"\$$1\" ]; } && $1=\${$1%\"\$CR\"}" && {
    lineno=$(($lineno + 1))
  }
}

one_line_syntax_check() { :; }

check_filter() {
  check_filter="$1"
  replace_all check_filter '$' "$DC1"
  replace_all check_filter '`' "$DC2"
  eval "set -- $check_filter"
  if [ $# -gt 0 ]; then
    check_filter="$1"
    replace_all check_filter "$DC1" '$'
    replace_all check_filter "$DC2" '`'
    match_pattern "$check_filter" "$SHELLSPEC_EXAMPLE_FILTER" && return 0
    shift
  fi
  [ "$SHELLSPEC_TAG_FILTER" ] || return 1
  while [ $# -gt 0 ]; do
    case $SHELLSPEC_TAG_FILTER in (*,$1,*) return 0; esac
    case $1 in
      *:*) case $SHELLSPEC_TAG_FILTER in (*,${1%%:*},*) return 0; esac ;;
      *  ) case $SHELLSPEC_TAG_FILTER in (*,$1,*) return 0 ; esac ;;
    esac
    shift
  done
  return 1
}

is_constant_name() {
  match "$1" "[!A-Z_]*" && return 1
  match "$1" "*[!A-Z0-9_]*" && return 1
  return 0
}

is_function_name() {
  match "$1" "[!a-zA-Z_]*" && return 1
  match "$1" "*[!a-zA-Z0-9_]*" && return 1
  return 0
}

is_comment() {
  case $1 in (\# | \#\ * | \#$TAB*) return 0; esac
  return 1
}

is_embedded_text() {
  case $1 in (\#\|*) return 0; esac
  return 1
}

if_embedded_text() {
  is_embedded_text "$1" || return 1
  set -- "$@" "${1#??}"
  shift
  "$@"
}

increase_block_id() {
  [ "$block_id_increased" ] && block_id=$block_id${block_id:+-}0
  case $block_id in
    *-*) block_id=${block_id%-*}-$((${block_id##*-} + 1)) ;;
    *  ) block_id=$(($block_id + 1)) ;;
  esac
  block_id_increased=1
}

decrease_block_id() {
  if [ "$block_id_increased" ]; then
    block_id_increased=''
  else
    block_id=${block_id%-*}
  fi
}

block_example_group() {
  if [ "$inside_of_example" ]; then
    syntax_error "Describe/Context cannot be defined inside of Example"
    return 0
  fi

  if ! one_line_syntax_check error "$1"; then
    syntax_error "Describe/Context has occurred an error" "$error"
    return 0
  fi

  check_filter "$1" && filter=1

  if [ "$block_id_increased" ]; then
    trans before_first_block "$block_id"
  fi
  increase_block_id
  _block_no=$(($_block_no + 1))
  block_no=$_block_no lineno_begin=$lineno
  eval "block_lineno_begin${_block_no}=$lineno"

  eval trans block_example_group ${1+'"$@"'}

  _block_no_stack="$_block_no_stack $_block_no" filter=''
  _parameter_count_stack="$_parameter_count_stack $parameter_no:$parameter_count"
}

block_example() {
  if [ "$inside_of_example" ]; then
    syntax_error "It/Example/Specify/Todo cannot be defined inside of Example"
    return 0
  fi

  parameters_need_example=''

  if ! one_line_syntax_check error "$1"; then
    syntax_error "It/Example/Specify/Todo has occurred an error" "$error"
    return 0
  fi

  check_filter "$1" && filter=1

  if [ "$block_id_increased" ]; then
    trans before_first_block "$block_id"
  fi
  increase_block_id
  _block_no=$(($_block_no + 1))
  block_no=$_block_no lineno_begin=$lineno
  eval "block_lineno_begin${block_no}=$lineno"

  eval trans block_example ${1+'"$@"'}

  _block_no_stack="$_block_no_stack $_block_no"
  example_no=$(($example_no + ${parameter_count:-1}))
  _parameter_count_stack="$_parameter_count_stack $parameter_no:$parameter_count"
  filter='' inside_of_example="yes"
}

block_end() {
  if [ ! "$_block_no_stack" ]; then
    syntax_error "Unexpected 'End'"
    return 0
  fi

  if [ "$parameters_need_example" ]; then
    syntax_error "Not found any examples. (Missing 'End' of Parameters?)"
    parameters_need_example=''
    return 0
  fi

  if [ ! "$block_id_increased" ]; then
    trans after_last_block "${block_id%-*}"
  fi
  decrease_block_id
  block_no=${_block_no_stack##* } lineno_end=$lineno
  trans after_block "$block_no"
  eval "block_lineno_end${block_no}=$lineno"
  eval "lineno_begin=\$block_lineno_begin${block_no}"

  if is_in_ranges; then
    enabled=1
    remove_from_ranges
  fi

  eval trans block_end ${1+'"$@"'}
  enabled=''

  _block_no_stack=${_block_no_stack% *}
  parameter_count=${_parameter_count_stack##* }
  parameter_no=${parameter_count%:*}
  parameter_count=${parameter_count#*:}
  _parameter_count_stack=${_parameter_count_stack% *}
  inside_of_example=""
}

x() {
  skipped=1 skip_id=$(($skip_id + 1))
  "$@"
  skipped=''
}

f() {
  focused="focus" filter=1
  "$@"
  focused='' filter=''
}

todo() {
  block_example "$1"
  pending "$1"
  block_end
}

evaluation() {
  if [ ! "$inside_of_example" ]; then
    syntax_error "When cannot be defined outside of Example"
    return 0
  fi
  eval trans evaluation ${1+'"$@"'}
}

expectation() {
  if [ ! "$inside_of_example" ]; then
    syntax_error "The cannot be defined outside of Example"
    return 0
  fi
  eval trans expectation ${1+'"$@"'}
}

example_hook() {
  if [ "$inside_of_example" ]; then
    syntax_error "Before/After cannot be defined inside of Example"
    return 0
  fi
  eval trans control ${1+'"$@"'}
}

example_all_hook() {
  if [ "$inside_of_example" ]; then
    syntax_error "BeforeAll/AfterAll cannot be defined inside of Example"
    return 0
  fi

  if [ "$1" = "before_all" ] && [ ! "$block_id_increased" ]; then
    syntax_error "BeforeAll cannot be defined after of Example Group/Example in same block"
    return 0
  fi

  eval trans control ${1+'"$@"'}
}

control() {
  eval trans control ${1+'"$@"'}
}

pending() {
  case ${1:-} in (\#*)
    temporary_pending=${1#"#"}
    escape_quote temporary_pending
    trim temporary_pending "$temporary_pending"
    set -- "'# $temporary_pending'"
  esac
  eval trans pending ${1+'"$@"'}
}

skip() {
  skip_id=$(($skip_id + 1))
  case ${1:-} in (\#*)
    temporary_skip=${1#"#"}
    escape_quote temporary_skip
    trim temporary_skip "$temporary_skip"
    set -- "'# $temporary_skip'"
  esac
  eval trans skip ${1+'"$@"'}
}

data() {
  eval trans data_begin ${1+'"$@"'}
  case ${2:-} in
    '' | \#* | \|*)
      trans embedded_text_begin "$1" "${2:-}"
      line='' error=
      while read_specfile line; do
        trim line "$line"
        is_comment "$line" && continue # ignore comment line
        if_embedded_text "$line" trans embedded_text_line && continue
        is_end_block "${line%% *}" && break
        [ "$error" ] && continue
        error=$(syntax_error "Data text should begin with '#|' or '# '")
      done
      trans embedded_text_end
      [ ! "$error" ] || putsn "$error"
      ;;
    \'* | \"*) trans data_text "$2" ;;
    \<*) trans data_file "$2" ;;
    *) trans data_func "$2" ;;
  esac
  eval trans data_end ${1+'"$@"'}
}

text_begin() {
  eval trans embedded_text_begin ${1+'"$@"'}
  inside_of_text=1
}

text_line() {
  if_embedded_text "$1" trans embedded_text_line && return 0
  text_end
  return 1
}

text_end() {
  eval trans embedded_text_end ${1+'"$@"'}
  inside_of_text=''
}

parameters() {
  if [ "$inside_of_example" ]; then
    syntax_error "Parameters cannot be defined inside of Example"
    return 0
  fi
  parameters_need_example=1

  parameter_no=$(($parameter_no + 1))
  trans parameters_begin "$parameter_no"
  #shellcheck disable=SC2145
  "parameters_$@"
  trans parameters_end
}

parameters_generate_code() {
  trans line "$1"
  code="${code}${1}${LF}"
}

parameters_continuation_line() {
  line=$1
  shift
  while ends_with "$line"  "\\"; do
    read_specfile line ||:
    "$@" "$line"
  done
}

parameters_block() {
  while read_specfile line; do
    trim line "$line"
    is_end_block "${line%% *}" && break
    case $line in (\#* | '') continue; esac

    trans parameters "$line"
    parameter_count=$(($parameter_count + 1))
    parameters_continuation_line "$line" trans line
  done
}

parameters_value() {
  code=''
  if [ $# -gt 0 ]; then
    IFS=" $IFS"
    parameters_generate_code "for shellspec_matrix in $*; do"
    IFS=${IFS#?}
    trans parameters "\"\$shellspec_matrix\""
    code="${code}count=\$((\$count + 1))${LF}"
    parameters_generate_code "done"
  fi
  eval "parameter_count=\$(count=0${LF}${code}echo \"\$count\")"
}

parameters_matrix() {
  code='' nest=0 arguments=''

  while read_specfile line; do
    trim line "$line"
    is_end_block "${line%% *}" && break
    case $line in (\#* | '') continue; esac

    nest=$(($nest + 1))
    parameters_generate_code "for shellspec_matrix${nest} in $line"
    arguments="$arguments\"\$shellspec_matrix${nest}\" "
    parameters_continuation_line "$line" parameters_generate_code
    parameters_generate_code "do"
  done

  trans parameters "$arguments"
  code="${code}count=\$((\$count + 1))${LF}"

  while [ $nest -gt 0 ]; do
    parameters_generate_code "done"
    nest=$(($nest - 1))
  done

  eval "parameter_count=\$(count=0${LF}${code}echo \"\$count\")"
}

parameters_dynamic() {
  code=''

  while read_specfile line; do
    trim line "$line"
    is_end_block "${line%% *}" && break

    case $line in
      %data | %data\ *)
        line=${line#%data}
        trans parameters "$line"
        line='count=$(($count + 1))'
        ;;
      *) trans line "$line"
    esac
    code="${code}${line}${LF}"
  done

  eval "parameter_count=\$(count=0${LF}{ $code }>&2;echo \"\$count\")"
}

mock() {
  inside_of_mock=1
  eval trans mock_begin ${1+'"$@"'}
  mock_no=$(($mock_no + 1))
}

mock_end() {
  is_end_block "${1%% *}" || return 1
  inside_of_mock=''
  eval trans mock_end ${1+'"$@"'}
}

constant() {
  if [ "$_block_no_stack" ]; then
    syntax_error "Constant should be defined outside of Example Group/Example"
    return 0
  fi

  trim line "$1"
  name=${line%%:*} value=''
  trim value "${line#*:}"
  if is_constant_name "$name"; then
    trans constant "$name" "$value"
    eval "$name=\$value"
  else
    syntax_error "Constant name should match pattern [A-Z_][A-Z0-9_]*"
  fi
}

include() {
  if [ "$inside_of_example" ]; then
    syntax_error "Include cannot be defined inside of Example"
    return 0
  fi

  if ! one_line_syntax_check error "$1"; then
    syntax_error "Include has occurred an error" "$error"
    return 0
  fi

  eval trans include ${1+'"$@"'}
}

with_function() {
  trans with_function "$1"
  shift
  "$@"
}

out() {
  eval trans out ${1+'"$@"'}
}

is_in_range() {
  case $1 in
    @*) [ "$block_id" = "${1#@}" ] ;;
    *) [ "$lineno_begin" -le "$1" ] && [ "$1" -le "$lineno_end" ] ;;
  esac
}

is_in_ranges() {
  [ "${ranges:-}" ] || return 1
  eval "set -- $ranges"
  while [ $# -gt 0 ]; do
    is_in_range "$1" && return 0
    shift
  done
  return 1
}

remove_from_ranges() {
  eval "set -- $ranges"
  ranges=''
  while [ $# -gt 0 ]; do
    is_in_range "$1" || ranges="$ranges$1 "
    shift
  done
}

translate() {
  work=''
  while read_specfile line; do
    while ends_with "$line" "\\"; do
      read_specfile work ||:
      line="${line}${LF}${work}"
    done
    trim work "$line"
    dsl=${work%% *} rest=''

    [ "$inside_of_text" ] && text_line "$work" && continue
    translate_mock "$dsl" && continue

    trim rest "${work#"$dsl"}"
    mapping "$dsl" "$rest" || trans line "$line"
  done
}

translate_mock() {
  if [ "$inside_of_mock" ]; then
    mock_end "$1" && return 0
    is_dsl "$1" && use_dsl_in_mock=1 && return 0
  elif [ "$use_dsl_in_mock" ]; then
    syntax_error "Only directives can be used in Mock"
  fi
  return 1
}