Code Monkey home page Code Monkey logo

shellspec's Introduction

ShellSpec

ShellSpec is a full-featured BDD unit testing framework for dash, bash, ksh, zsh and all POSIX shells that provides first-class features such as code coverage, mocking, parameterized test, parallel execution and more. It was developed as a dev/test tool for cross-platform shell scripts and shell script libraries. ShellSpec is a new modern testing framework released in 2019, but it's already stable enough. With lots of practical CLI features and simple yet powerful syntax, it provides you with a fun shell script test environment.

GitHub Actions Status Travis CI Cirrus CI Circle CI Docker Cloud Automated buildDocker Cloud Build Status
Kcov Coveralls Code Climate Codecov CodeFactor Grade GitHub top language GitHub release License

bash bosh busybox dash ksh mksh posh yash zsh


Version 0.28.0 has a lot of enhancements in the CLI. It is basically compatible, but there are some changes that you need to be aware of. See Migration Guide to Version 0.28.0 for details.


Thank you for your interest in ShellSpec. Please visit 🚩the official website to know the impressive features!

Let's have fun testing your shell scripts! (Try Online Demo on your browser).

demo

Coverage report

Latest Update.

See CHANGELOG.md

NOTE: This documentation contains unreleased features. Check them in the changelog.


Table of Contents

Supported shells and platforms

Platform Test
Linux (Debian, Ubuntu, Fedora, CentOS, Alpine, Busybox, OpenWrt) GitHub Actions or Docker
macOS (Default installed shells, Homebrew) GitHub Actions or Travis CI
Windows (Git bash, msys2, cygwin, busybox-w32, WSL) GitHub Actions
BSD (FreeBSD, OpenBSD, NetBSD) Cirrus CI (FreeBSD) or Manual (Others)
Unix (Solaris, AIX) Manual only

Tested version details

Requirements

POSIX-compliant commands

ShellSpec uses shell built-in commands and only few basic POSIX-compliant commands to support wide range of environments (except kcov for optional code coverage).

Currently used external (not shell builtins) commands:

  • cat, date, env, ls, mkdir, od (or not POSIX hexdump), rm, sleep, sort, time
  • ps (use to auto-detect shells in environments that don't implement procfs)
  • ln, mv (use only when generating coverage report)
  • kill, printf (most shells except some are built-in)

Installation

Web installer (for developers)

Install the latest release version

curl -fsSL https://git.io/shellspec | sh

or

wget -O- https://git.io/shellspec | sh

NOTE: https://git.io/shellspec is redirected to install.sh

The installation using the web installer is mainly intended for development use. For CI, it is recommended to use a specific version (tag) in git or archives to avoid unexpected failures.

Advanced installation / upgrade

Automatic installation

curl -fsSL https://git.io/shellspec | sh -s -- --yes

Install the specified version

curl -fsSL https://git.io/shellspec | sh -s 0.19.1

Upgrade to the latest release version

curl -fsSL https://git.io/shellspec | sh -s -- --switch

Switch to the specified version

curl -fsSL https://git.io/shellspec | sh -s 0.18.0 --switch
Uninstall

How to uninstall

  1. Delete the ShellSpec executable file [default: $HOME/.local/bin/shellspec].
  2. Delete the ShellSpec installation directory [default: $HOME/.local/lib/shellspec].
Other usage

Other usage

$ curl -fsSL https://git.io/shellspec | sh -s -- --help
Usage: [sudo] ./install.sh [VERSION] [OPTIONS...]
  or : wget -O- https://git.io/shellspec | [sudo] sh
  or : wget -O- https://git.io/shellspec | [sudo] sh -s -- [OPTIONS...]
  or : wget -O- https://git.io/shellspec | [sudo] sh -s VERSION [OPTIONS...]
  or : curl -fsSL https://git.io/shellspec | [sudo] sh
  or : curl -fsSL https://git.io/shellspec | [sudo] sh -s -- [OPTIONS...]
  or : curl -fsSL https://git.io/shellspec | [sudo] sh -s VERSION [OPTIONS...]

VERSION:
  Specify install version and method

  e.g
    1.0.0           Install 1.0.0 from git
    master          Install master from git
    1.0.0.tar.gz    Install 1.0.0 from tar.gz archive
    .               Install from local directory

OPTIONS:
  -p, --prefix PREFIX   Specify prefix                 [default: $HOME/.local]
  -b, --bin BIN         Specify bin directory          [default: <PREFIX>/bin]
  -d, --dir DIR         Specify installation directory [default: <PREFIX>/lib/shellspec]
  -s, --switch          Switch version (requires installation via git)
  -l, --list            List available versions (tags)
      --pre             Include pre-release
      --fetch FETCH     Force command to use when installing from archive (curl or wget)
  -y, --yes             Automatic yes to prompts
  -h, --help            You're looking at it

Package manager

Arch Linux

Installation on Arch Linux from the AUR ShellSpec package using aura:

# Install the latest stable version
$ aura -A shellspec
Homebrew / Linuxbrew
# Install the latest stable version
$ brew tap shellspec/shellspec
$ brew install shellspec
basher / bpkg

Installation with basher

The officially supported version is ShellSpec 0.19.1 and later.

# Install from master branch
$ basher install shellspec/shellspec

# To specify a version (example: 0.19.1)
$ basher install shellspec/[email protected]

Installation with bpkg

The officially supported version is ShellSpec 0.19.1 and later.

# Install from master branch
$ bpkg install shellspec/shellspec

# To specify a version (example: 0.19.1)
$ bpkg install shellspec/[email protected]

Manual installation

git / archive (source code)

Download from git or archive and create a symbolic link.

From git

$ cd /SOME/WHERE/TO/INSTALL
$ git clone https://github.com/shellspec/shellspec.git

$ ln -s /SOME/WHERE/TO/INSTALL/shellspec/shellspec /EXECUTABLE/PATH/

From archive

$ cd /SOME/WHERE/TO/INSTALL
$ wget https://github.com/shellspec/shellspec/archive/{VERSION}.tar.gz
$ tar xzvf shellspec-{VERSION}.tar.gz

$ ln -s /SOME/WHERE/TO/INSTALL/shellspec-{VERSION}/shellspec /EXECUTABLE/PATH/

Executable path: e.g. /usr/local/bin/, $HOME/bin/

Use make instead of symbolic link creation

Download from git or archive and use make command.

How to install.

Install to /usr/local/bin and /usr/local/lib

sudo make install

Install to $HOME/bin and $HOME/lib

make install PREFIX=$HOME

How to uninstall.

sudo make uninstall
make uninstall PREFIX=$HOME
For environments that do not support symbolic links

Download from git or archive and create the following shellspec file instead of the symbolic link.

$ cat<<'HERE'>/EXECUTABLE/PATH/shellspec
#!/bin/sh
exec /SOME/WHERE/TO/INSTALL/shellspec/shellspec "$@"
HERE
$ chmod +x /EXECUTABLE/PATH/shellspec

Distribution archive (runtime only)

See Releases page if you want to download distribution archive.

Tutorial

Just create your project directory and run shellspec --init to setup your project

# Create your project directory, for example "hello".
$ mkdir hello
$ cd hello

# Initialize
$ shellspec --init
  create   .shellspec
  create   spec/spec_helper.sh

# Write your first specfile (of course you can use your favorite editor)
$ cat<<'HERE'>spec/hello_spec.sh
Describe 'hello.sh'
  Include lib/hello.sh
  It 'says hello'
    When call hello ShellSpec
    The output should equal 'Hello ShellSpec!'
  End
End
HERE

# Create lib/hello.sh
$ mkdir lib
$ touch lib/hello.sh

# It will fail because the hello function is not implemented.
$ shellspec

# Write hello function
$ cat<<'HERE'>lib/hello.sh
hello() {
  echo "Hello ${1}!"
}
HERE

# It will success!
$ shellspec

ShellSpec CLI

runs specfile using /bin/sh by default

ShellSpec CLI runs specfiles with the shell running shellspec. Usually it is /bin/sh that is the shebang of shellspec. If you run bash shellspec, it will be bash. Include files from specfile will be executed in the same shell as well.

The purpose of this specification is to allow ShellSpec to easily change multiple types of shells and enable the development of cross-platform shell scripts that support multiple shells and environments.

If you want to test with a specific shell, use the -s (--shell) option. You can specify the default shell in the .shellspec file.

NOTE: If you execute a shell script file (not a shell function) from within the specfile, its shebang will be respected. Because in that case, it will be run as an external command. The -s (--shell) option also has no effect. If you are testing a external shell script file, you can use When run script or When run source. These ignore the shebang of external shell script file and run in the same shell that runs specfile.

command options

NOTE: Since version 0.28.0, getoptions is used to parse options, so all POSIX and GNU compatible option syntax can be used. For example, you can abbreviate a long option.

See more info: ShellSpec CLI

$ shellspec -h
Usage: shellspec [ -c ] [-C <directory>] [options...] [files or directories...]

  Using + instead of - for short options causes reverses the meaning

    -s, --shell SHELL               Specify a path of shell [default: "auto" (the shell running shellspec)]
        --require MODULE            Require a MODULE (shell script file)
    -O, --options PATH              Specify the path to an additional options file
    -I, --load-path PATH            Specify PATH to add to $SHELLSPEC_LOAD_PATH (may be used more than once)
        --helperdir DIRECTORY       The directory to load helper files (spec_helper.sh, etc) [default: "spec"]
        --path PATH                 Set PATH environment variable at startup
        --{no-}sandbox              Force the use of the mock instead of the actual command
        --sandbox-path PATH         Make PATH the sandbox path instead of empty [default: empty]
        --execdir @LOCATION[/DIR]   Specify the execution directory of each specfile | [default: @project]
    -e, --env NAME[=VALUE]          Set environment variable
        --env-from ENV-SCRIPT       Set environment variable from shell script file
    -w, --{no-}warning-as-failure   Treat warning as failure [default: enabled]
        --{no-}fail-fast[=COUNT]    Abort the run after first (or COUNT) of failures [default: disabled]
        --{no-}fail-no-examples     Fail if no examples found [default: disabled]
        --{no-}fail-low-coverage    Fail on low coverage [default: disabled]
        --failure-exit-code CODE    Override the exit code used when there are failing specs [default: 101]
        --error-exit-code CODE      Override the exit code used when there are fatal errors [default: 102]
    -p, --{no-}profile              Enable profiling and list the slowest examples [default: disabled]
        --profile-limit N           List the top N slowest examples [default: 10]
        --{no-}boost                Increase the CPU frequency to boost up testing speed [default: disabled]
        --log-file LOGFILE          Log file for %logger directive and trace [default: "/dev/tty"]
        --tmpdir TMPDIR             Specify temporary directory [default: $TMPDIR, $TMP or "/tmp"]
        --keep-tmpdir               Do not cleanup temporary directory [default: disabled]

  The following options must be specified before other options and cannot be specified in the options file

    -c, --chdir                     Change the current directory to the first path of arguments at the start
    -C, --directory DIRECTORY       Change the current directory at the start

  **** Execution ****

    -q, --{no-}quick                Run not-passed examples if it exists, otherwise run all [default: disabled]
    -r, --repair, --only-failures   Run failure examples only (Depends on quick mode)
    -n, --next-failure              Run failure examples and abort on first failure (Depends on quick mode)
    -j, --jobs JOBS                 Number of parallel jobs to run [default: 0 (disabled)]
        --random TYPE[:SEED]        Run examples by the specified random type | <[none]> [specfiles] [examples]
    -x, --xtrace                    Run examples with trace output of evaluation enabled [default: disabled]
    -X, --xtrace-only               Run examples with trace output only enabled [default: disabled]
        --dry-run                   Print the formatter output without running any examples [default: disabled]

  **** Output ****

        --{no-}banner               Show banner if exist "<HELPERDIR>/banner[.md]" [default: enabled]
        --reportdir DIRECTORY       Output directory of the report [default: "report"]
    -f, --format FORMATTER          Choose a formatter for display | <[p]> [d] [t] [j] [f] [null] [debug]
    -o, --output FORMATTER          Choose a generator(s) to generate a report file(s) [default: none]
        --{no-}color                Enable or disable color [default: enabled if the output is a TTY]
        --skip-message VERBOSITY    Mute skip message | <[verbose]> [moderate] [quiet]
        --pending-message VERBOSITY Mute pending message | <[verbose]> [quiet]
        --quiet                     Equivalent of --skip-message quiet --pending-message quiet
        --(show|hide)-deprecations  Show or hide deprecations details [default: show]

  **** Ranges / Filters / Focus ****

    You can run selected examples by specified the line numbers or ids

      shellspec path/to/a_spec.sh:10   # Run the groups or examples that includes lines 10
      shellspec path/to/a_spec.sh:@1-5 # Run the 5th groups/examples defined in the 1st group
      shellspec a_spec.sh:10:@1:20:@2  # You can mixing multiple line numbers and ids with join by ":"

    -F, --focus                     Run focused groups / examples only
    -P, --pattern PATTERN           Load files matching pattern [default: "*_spec.sh"]
    -E, --example PATTERN           Run examples whose names include PATTERN
    -T, --tag TAG[:VALUE]           Run examples with the specified TAG
        --default-path PATH         Set the default path where looks for examples [default: "spec"]

    You can specify the path recursively by prefixing it with the pattern "*/" or "**/"
      (This is not glob patterns and requires quotes. It is also available with --default-path)

      shellspec "*/spec"               # The pattern "*/" matches 1 directory
      shellspec "**/spec"              # The pattern "**/" matches 0 and more directories
      shellspec "*/*/**/test_spec.sh"  # These patterns can be specified multiple times

    -L, --dereference               Dereference all symlinks in in the above pattern [default: disabled]

  **** Coverage ****

        --covdir DIRECTORY          Output directory of the Coverage Report [default: coverage]
        --{no-}kcov                 Enable coverage using kcov [default: disabled]
        --kcov-path PATH            Specify kcov path [default: kcov]
        --kcov-options OPTIONS      Additional Kcov options (coverage limits, coveralls id, etc)

  **** Utility ****

        --init [TEMPLATE...]        Initialize your project with ShellSpec | [spec] [git] [hg] [svn]
        --gen-bin [@COMMAND...]     Generate test support commands in "<HELPERDIR>/support/bin"
        --count                     Count the number of specfiles and examples
        --list LIST                 List the specfiles/examples | [specfiles] [examples(:id|:lineno)]
        --syntax-check              Syntax check of the specfiles without running any examples
        --translate                 Output translated specfile
        --task [TASK]               Run the TASK or Show the task list if TASK is not specified
        --docker DOCKER-IMAGE       Run tests in specified docker image (EXPERIMENTAL)
    -v, --version                   Display the version
    -h, --help                      -h: short help, --help: long help

Project directory

All specfiles for ShellSpec must be under the project directory. The root of the project directory must have a .shellspec file. This file is that specify the default options to be used in the project, but an empty file is required even if the project has no options.

NOTE: The .shellspec file was described in the documentation as a required file for some time, but ShellSpec worked without it. Starting with version 0.28.0, this file is checked and will be required in future versions.

You can easily create the necessary files by executing the shellspec --init command in an existing directory.

Typical directory structure

This is the typical directory structure. Version 0.28.0 allows many of these to be changed by specifying options, supporting a more flexible directory structure.

<PROJECT-ROOT> directory
├─ .shellspec                       [mandatory]
├─ .shellspec-local                 [optional] Ignore from version control
├─ .shellspec-quick.log             [optional] Ignore from version control
├─ report/                          [optional] Ignore from version control
├─ coverage/                        [optional] Ignore from version control
│
├─ bin/
│   ├─ your_script1.sh
│              :
├─ lib/
│   ├─ your_library1.sh
│              :
│
├─ spec/ (also <HELPERDIR>)
│   ├─ spec_helper.sh               [recommended]
│   ├─ banner[.md]                  [optional]
│   ├─ support/                     [optional]
│   │
│   ├─ bin/
│   │   ├─ your_script1_spec.sh
│   │             :
│   ├─ lib/
│   │   ├─ your_library1_spec.sh

Options file

To change the default options for the shellspec command, create options file(s). Files are read in the order shown below, options defined last take precedence.

  1. $XDG_CONFIG_HOME/shellspec/options
  2. $HOME/.shellspec-options (version >= 0.28.0) or $HOME/.shellspec (deprecated)
  3. <PROJECT-ROOT>/.shellspec
  4. <PROJECT-ROOT>/.shellspec-local (Do not store in VCS such as git)

Specify your default options with $XDG_CONFIG_HOME/shellspec/options or $HOME/.shellspec-options. Specify default project options with .shellspec and overwrite to your favorites with .shellspec-local.

.shellspec - project options file

Specifies the default options to use for the project.

.shellspec-local - user custom options file

Override the default options used by the project with your favorites.

.shellspec-basedir - specfile execution base directory

Used to specify the directory in which the specfile will be run. See directory structure or --execdir option for details.

.shellspec-quick.log - quick execution log

If this file is present, Quick mode will be enabled and the log of Quick execution will be recorded. It created automatically when --quick option is specified. If you want to turn off Quick mode, delete it.

report/ - report file directory

The output location for reports generated by the --output or --profile options. This can be changed with the --reportdir option.

coverage/ - coverage reports directory

The output location for coverage reports. This can be changed with the --covdir option.

spec/ - (default) specfiles directory

By default, it is assumed that all specfiles are store under the spec directory, but it is possible to create multiple directories with different names.

NOTE: In Version <= 0.27.x, the spec directory was the only directory that contained the specfiles.

<HELPERDIR> (default: spec/)

The directory to store spec_helper.sh and other files. By default, the spec directory also serves as HELPERDIR directory, but you can change it to another directory with the --helperdir option.

spec_helper.sh - (default) helper file for specfile

The spec_helper.sh is loaded to specfile by the --require spec_helper option. This file is used to define global functions, initial setting for examples, custom matchers, etc.

banner[.md] - banner file displayed at test execution

If the file <HELPERDIR>/banner or <HELPERDIR>/banner.md exists, Display a banner when the shellspec command is executed. It can be used to display information about the tests. The --no-banner option can be used to disable this behavior.

support/ - directory for support files

This directory can be used to store files such as custom matchers and tasks.

bin - directory for support commands

This directory is used to store support commands.

Specfile (test file)

In ShellSpec, you write your tests in a specfile. By default, specfile is a file ending with _spec.sh under the spec directory.

The specfile is executed using the shellspec command, but it can also be executed directly. See self-executable specfile for details.

Example

Describe 'lib.sh' # example group
  Describe 'bc command'
    add() { echo "$1 + $2" | bc; }

    It 'performs addition' # example
      When call add 2 3 # evaluation
      The output should eq 5  # expectation
    End
  End
End

The best place to learn how to write a specfile is the examples/spec directory. You should take a look at it! (Those examples include failure examples on purpose.)

About DSL

ShellSpec has its own DSL to write tests. It may seem like a distinctive code because DSL starts with a capital letter, but the syntax is compatible with shell scripts, and you can embed shell functions and use ShellCheck to check the syntax.

You may feel rejected by this DSL, but It starts with a capital letter to avoid confusion with the command, and it does a lot more than you think, such as realizing scopes, getting shell-independent line numbers, and workarounds for bugs in some shells.

Execution directory

Since version 0.28.0, the working directory when running a specfile is the project root directory by default. Even if you run a specfile from a subdirectory in the project directory, it is the project root directory. Before 0.27.x, it was the current directory when the shellspec command was executed.

You can change this directory (location) by using the --execdir @LOCATION[/DIR] option. You can choose from the following locations and specify a path relative to the location if necessary. However, you cannot specify a directory outside the project directory.

  • @project Where the .shellspec file is located (project root) [default]
  • @basedir Where the .shellspec or .shellspec-basedir file is located
  • @specfile Where the specfile is located

If @basedir is specified, the parent directory is searched from the directory containing the specfile to be run, and the first directory where .shellspec-basedir or .shellspec is found is used as the execution directory. This is useful if you want to have a separate directory for each utility (command) you want to test.

NOTE: You will need to change under the project directory or use the -c (--chdir) or -C (--directory) option before running the specfile.

Embedded shell scripts

You can embed shell functions (or shell script code) in the specfile. This shell function can be used for test preparation and complex testing.

Note that the specfile implements scope using subshells. Shell functions defined in the specfile can only be used within blocks (e.g. Describe, It, etc).

If you want to use a global function, you can define it in spec_helper.sh.

Translation process

The specfile will not be executed directly by the shell, but will be translated into a regular shell script and output to a temporary directory (default: /tmp) before being executed.

The translation process is simple in that it only replaces forward-matched words (DSLs), with a few exceptions. If you are interested in the translated code, you can see with shellspec --translate.

Syntax formatter (altshfmt)

The specfile contains DSLs, so it cannot be properly formatted by general shell script formatters. If you want to format the specfile, use altshfmt.

DSL syntax

Basic structure

Describe, Context, ExampleGroup - example group block

ExampleGroup is a block for grouping example groups or examples. Describe and Context are aliases for ExampleGroup. They can be nested, and may contain example groups or examples.

Describe 'is example group'
  Describe 'is nestable'
    ...
  End

  Context 'is used to facilitate understanding depending on the context'
    ...
  End
End

The example groups can be optionally tagged. See Tagging for details.

Describe 'is example group' tag1:value1 tag2:value2 ...

It, Specify, Example - example block

Example is a block for writing an evaluation and expectations. It and Specify are aliases for Example.

An example is composed by up to one evaluation and multiple expectations.

add() { echo "$1 + $2" | bc; }

It 'performs addition'          # example
  When call add 2 3             # evaluation
  The output should eq 5        # expectation
  The status should be success  # another expectation
End

The examples can be optionally tagged. See Tagging for details.

It 'performs addition' tag1:value1 tag2:value2 ...

Todo - one liner empty example

Todo is the same as the empty example and is treated as a pending example.

Todo 'will be used later when we write a test'

It 'is an empty example, the same as Todo'
End

When - evaluation

Evaluation executes a shell function or command for verification. Only one evaluation can be defined for each example and also can be omitted.

See more details of Evaluation

NOTE: About executing aliases

call - call a shell function (without subshell)

It calls a function without subshell. Practically, it can also run commands.

When call add 1 2 # call `add` shell function with two arguments.
run - run a command (within subshell)

It runs a command within subshell. Practically, it can also call a shell function. The command does not have to be a shell script.

NOTE: This does not support coverage measurement.

When run touch /tmp/foo # run `touch` command.

Some commands below are specially handled by ShellSpec.

command - runs an external command

It runs a command, respecting shebang. It can not call a shell function. The command does not have to be a shell script.

NOTE: This does not support coverage measurement.

When run command touch /tmp/foo # run `touch` command.
script - runs a shell script

It runs a shell script, ignoring shebang. The script has to be a shell script. It will be executed in another instance of the same shell as the current shell.

When run script my.sh # run `my.sh` script.
source - runs a script by . (dot) command

It sources a shell script, ignoring its shebang. The script has to be a shell script. It is similar to run script, but with some differences. Unlike run script, function-based mocking is available.

When run source my.sh # source `my.sh` script.
About executing aliases

If you want to execute aliases, you need a workaround using eval.

alias alias-name='echo this is alias'
When call alias-name # alias-name: not found

# eval is required
When call eval alias-name

# When using embedded shell scripts
foo() { eval alias-name; }
When call foo

The - expectation

Expectation begins with The which does the verification. The basic syntax is as follows:

The output should equal 4

Use should not for the opposite verification.

The output should not equal 4
Subjects

The subject is the target of the verification.

The output should equal 4
      |
      +-- subject

There are output (stdout), error (stdout), status, variable, path, etc. subjects.

Please refer to the Subjects for more details.

Modifiers

The modifier concretizes the target of the verification (subject).

The line 2 of output should equal 4
      |
      +-- modifier

The modifiers are chainable.

The word 1 of line 2 of output should equal 4

If the modifier argument is a number, you can use an ordinal numeral instead of a number.

The first word of second line of output should equal 4

There are line, word, length, contents, result, etc. modifiers. The result modifier is useful for making the result of a user-defined function the subject.

Please refer to the Modifiers for more details.

Matchers

The matcher is the verification.

The output should equal 4
                   |
                   +-- matcher

There are many matchers such as string matcher, status matcher, variable matchers and stat matchers. The satisfy matcher is useful for verification with user-defined functions.

Please refer to the Matchers for more details.

Language chains

ShellSpec supports language chains like chai.js. It only improves readability, does not affect the expectation: a, an, as, the.

The following two sentences have the same meaning:

The first word of second line of output should valid number

The first word of the second line of output should valid as a number

Assert - expectation for custom assertion

The Assert is yet another expectation to verify with a user-defined function. It is designed for verification of side effects, not the result of the evaluation.

still_alive() {
  ping -c1 "$1" >/dev/null
}

Describe "example.com"
  It "responses"
    Assert still_alive "example.com"
  End
End

Pending, skip and focus

Pending - pending example

Pending is similar to Skip, but the test passes if the verification fails, and the test fails if the verification succeeds. This is useful if you want to specify that you will implement something later.

Describe 'Pending'
  Pending "not implemented"

  hello() { :; }

  It 'will success when test fails'
    When call hello world
    The output should "Hello world"
  End
End

Skip - skip example

Use Skip to skip executing the example.

Describe 'Skip'
  Skip "not exists bc"

  It 'is always skip'
    ...
  End
End
if - conditional skip

Use Skip if if you want to skip conditionally.

Describe 'Conditional skip'
  not_exists_bc() { ! type bc >/dev/null 2>&1; }
  Skip if "not exists bc" not_exists_bc

  add() { echo "$1 + $2" | bc; }

  It 'performs addition'
    When call add 2 3
    The output should eq 5
  End
End

'x' prefix for example group and example

xDescribe, xContext, xExampleGroup - skipped example group

xDescribe, xContext, xExampleGroup are skipped example group blocks. Execution of examples contained in these blocks is skipped.

Describe 'is example group'
  xDescribe 'is skipped example group'
    ...
  End
End
xIt, xSpecify, xExample - skipped example

xIt, xSpecify, xExample are skipped example blocks. Execution of the example is skipped.

xIt 'is skipped example'
  ...
End

'f' prefix for example group and example

fDescribe, fContext, fExampleGroup - focused example group

fDescribe, fContext, fExampleGroup are focused example group blocks. Only the examples included in these will be executed when the --focus option is specified.

Describe 'is example group'
  fDescribe 'is focus example group'
    ...
  End
End
fIt, fSpecify, fExample - focused example

fIt, fSpecify, fExample are focused example blocks. Only these examples will be executed when the --focus option is specified.

fIt 'is focused example'
  ...
End

About temporary pending and skip

Using Pending or Skip without a message is a "temporary pending" or "temporary skip". "x"-prefixed example groups and examples are also treated as temporary skips.

The non-temporary Pending and Skip (with a message) are used when the case will take a long time to resolve. It may be committed to a version control system. Temporary pending and skip are used during current work. We do not recommend committing them to a version control system.

These two types differ in the display of the report. Refer to --skip-message and --pending-message options.

# Temporary pending and skip
Pending
Skip
Skip # this comment will be displayed in the report
Todo
xIt
  ...
End

# Non-temporary pending and skip
Pending "reason"
Skip "reason"
Skip if "reason" condition
Todo "It will be implemented"

Hooks

BeforeEach (Before), AfterEach (After) - example hook

You can specify commands to be executed before / after each example by BeforeEach (Before), AfterEach (After).

NOTE: BeforeEach and AfterEach are supported in version 0.28.0 and later. Previous versions should use Before and After instead.

NOTE: AfterEach is for cleanup and not for assertions.

Describe 'example hook'
  setup() { :; }
  cleanup() { :; }
  BeforeEach 'setup'
  AfterEach 'cleanup'

  It 'is called before and after each example'
    ...
  End

  It 'is called before and after each example'
    ...
  End
End

BeforeAll, AfterAll - example group hook

You can specify commands to be executed before / after all examples by BeforeAll and AfterAll.

Describe 'example all hook'
  setup() { :; }
  cleanup() { :; }
  BeforeAll 'setup'
  AfterAll 'cleanup'

  It 'is called before/after all example'
    ...
  End

  It 'is called before/after all example'
    ...
  End
End

BeforeCall, AfterCall - call evaluation hook

You can specify commands to be executed before / after call evaluation by BeforeCall and AfterCall.

NOTE: These hooks were originally created to test ShellSpec itself. Please use the BeforeEach / AfterEach hooks whenever possible.

Describe 'call evaluation hook'
  setup() { :; }
  cleanup() { :; }
  BeforeCall 'setup'
  AfterCall 'cleanup'

  It 'is called before/after call evaluation'
    When call hello world
    ...
  End
End

BeforeRun, AfterRun - run evaluation hook

You can specify commands to be executed before / after run evaluation (run, run command, run script, and run source) by BeforeRun and AfterRun.

These hooks are executed in the same subshell as the "run evaluation". Therefore, you can access the variables after executing the evaluation.

NOTE: These hooks were originally created to test ShellSpec itself. Please use the BeforeEach / AfterEach hooks whenever possible.

Describe 'run evaluation hook'
  setup() { :; }
  cleanup() { :; }
  BeforeRun 'setup'
  AfterRun 'cleanup'

  It 'is called before/after run evaluation'
    When run hello world
    ...
  End
End

Pitfalls

The hooks may fail in subtle ways if there is output to stderr, even if the return code / exit code is 0.

Commands like git checkout routinely write to stderr, even if there was no actual failure, so be aware that your hooks may fail because of this.

Helpers

Dump - dump stdout, stderr, and status for debugging

Dump stdout, stderr, and status of the evaluation. It is useful for debugging.

When call echo hello world
Dump # stdout, stderr and status

Include - include a script file

Include a shell script to test.

Describe 'lib.sh'
  Include lib.sh # hello function defined

  Describe 'hello()'
    It 'says hello'
      When call hello ShellSpec
      The output should equal 'Hello ShellSpec!'
    End
  End
End

Set - set shell options

Set shell options before executing each example. The shell option name is the long name of set or the name of shopt:

NOTE: Use Set instead of the set command because the set command may not work as expected in some shells.

Describe 'Set helper'
  Set 'errexit:off' 'noglob:on'

  It 'sets shell options before executing the example'
    When call foo
  End
End

Path, File, Dir - path alias

Path is used to define a short pathname alias. File and Dir are aliases for Path.

Describe 'Path helper'
  Path hosts-file="/etc/hosts"

  It 'defines short alias for long path'
    The path hosts-file should exist
  End
End

Data - pass data as stdin to evaluation

You can use the Data Helper which inputs data from stdin for evaluation. The input data is specified after #| in the Data or Data:expand block.

Describe 'Data helper'
  It 'provides with Data helper block style'
    Data # Use Data:expand instead if you want expand variables.
      #|item1 123
      #|item2 456
      #|item3 789
    End
    When call awk '{total+=$2} END{print total}'
    The output should eq 1368
  End
End

You can also use a file, function or string as data sources.

See more details of Data

Parameters - parameterized example

Parameterized tests (aka Data-driven testing) are used to run the same test once for each set of parameters. Parameters defines a block of parameters.

Describe 'example'
  Parameters
    "#1" 1 2 3
    "#2" 1 2 3
  End

  Example "example $1"
    When call echo "$(($2 + $3))"
    The output should eq "$4"
  End
End

In addition to the default Parameters block, three additional styles are supported: Parameters:value, Parameters:matrix and Parameters:dynamic.

See more details of Parameters

NOTE: You can also combine the Parameters and Data:expand helpers.

Mock - create a command-based mock

See Command-based mock

Intercept - create an intercept point

See Intercept

Directives

Directives are instructions that can be used in embedded shell scripts. It is used to solve small problems of shell scripts in testing.

This is like a shell function, but not a shell function. Therefore, the supported grammar is limited and can only be used at the beginning of a function definition or at the beginning of a line.

foo() { %puts "foo"; } # supported

bar() {
  %puts "bar" # supported
}

baz() {
  any command; %puts "baz" # not supported
}

%const (%) - constant definition

%const (% is short hand) directive defines a constant value. The characters which can be used for variable names are uppercase letters [A-Z], digits [0-9] and underscore _ only. It can not be defined inside an example group nor an example.

The value is evaluated during the specfile translation process. So you can access ShellSpec variables, but you can not access variable or function in the specfile.

This feature assumes use with conditional skip. The conditional skip may run outside of the examples. As a result, sometimes you may need variables defined outside of the examples.

%text - embedded text

You can use the %text directive instead of a hard-to-use heredoc with indented code. The input data is specified after #|.

Describe '%text directive'
  It 'outputs texts'
    output() {
      echo "start" # you can write code here
      %text
      #|aaa
      #|bbb
      #|ccc
      echo "end" # you can write code here
    }

    result() { %text
      #|start
      #|aaa
      #|bbb
      #|ccc
      #|end
    }

    When call output
    The output should eq "$(result)"
    The line 3 of output should eq 'bbb'
  End
End

%puts (%-), %putsn (%=) - output a string (with newline)

%puts (put string) and %putsn (put string with newline) can be used instead of (not portable) echo. Unlike echo, it does not interpret escape sequences regardless of the shell. %- is an alias of %puts, %= is an alias of %putsn.

%printf - alias for printf

This is the same as printf, but it can be used in the sandbox mode because the path has been resolved.

%sleep - alias for sleep

This is the same as sleep, but it can be used in the sandbox mode because the path has been resolved.

%preserve - preserve variables

Use the %preserve directive to preserve the variables in subshells and external shell scripts.

In the following cases, %preserve is required because variables are not preserved.

  • When run evaluation - It runs in a subshell.
  • Command-based mock (Mock) - It is an external shell script.
  • Function-based Mock called by command substitution
Describe '%preserve directive'
  It 'preserves variables'
    func() { foo=1; bar=2; baz=3; }
    preserve() { %preserve bar baz:BAZ; }
    AfterRun preserve

    When run func
    The variable foo should eq 1 # This will be failure
    The variable bar should eq 2 # This will be success
    The variable BAZ should eq 3 # Preserved to different variable (baz:BAZ)
  End
End

%logger - debug output

Output log messages to the log file (default: /dev/tty) for debugging.

%data - define parameter

See Parameters:dynamic.

Mocking

There are two ways to create a mock, (shell) function-based mock and (external) command-based mock. The function-based mock is usually recommended for performance reasons. Both can be overwritten with an internal block and will be restored when the block ends.

Function-based mock

The (shell) function-based mock is simply (re)defined with a shell function.

Describe 'function-based mock'
  get_next_day() { echo $(($(date +%s) + 86400)); }

  date() {
    echo 1546268400
  }

  It 'calls the date function'
    When call get_next_day
    The stdout should eq 1546354800
  End
End

Command-based mock

The (external) command-based mock creates a temporary mock shell script and runs as an external command. This is slow, but there are some advantages over the function-based mock.

  • Can be use invalid characters as the shell function name.
    • e.g. docker-compose (- cannot be used as a function name in POSIX)
  • Can be invoke a mocked command from an external command (not limited to shell script).

A command-based mock creates an external shell script with the contents of a Mock block, so there are some restrictions.

  • It is not possible to mock shell functions or shell built-in functions.
  • It is not possible to call shell functions outside the Mock block.
    • Exception: Can be called exported (export -f) functions. (bash only)
  • To reference variables outside the Mock block, they must be exported.
  • To return a variable from a Mock block, you need to use the %preserve directive.
Describe 'command-based mock'
  get_next_day() { echo $(($(date +%s) + 86400)); }

  Mock date
    echo 1546268400
  End

  It 'runs the mocked date command'
    When call get_next_day
    The stdout should eq 1546354800
  End
End

NOTE: To achieve this feature, a directory for mock commands is included at the beginning of the PATH.

Support commands

Execute the actual command within a mock function

Support commands are helper commands that can be used in the specfile. For example, it can be used in a mock function to execute the actual command. It is recommended that the support command name be the actual command name prefixed with @.

Describe "Support commands example"
  touch() {
    @touch "$@" # @touch executes actual touch command
    echo "$1 was touched"
  }

  It "touch a file"
    When run touch "file"
    The output should eq "file was touched"
    The file "file" should exist
  End
End

Support commands are generated in the spec/support/bin directory by the --gen-bin option. For example run shellspec --gen-bin @touch to generate the @touch command.

This is the main purpose, but support commands are just shell scripts, so they can also be used for other purposes. You can freely edit the support command script.

Make mock not mandatory in sandbox mode

The sandbox mode forces the use of mocks. However, you may not want to require mocks for some commands. For example, printf is a built-in command in many shells and does not require a mock in the sandbox mode for these shells. But there are shells where it is an external command and then it requires to be mocked.

To allow printf to be called without mocking in certain cases, create a support command named printf (shellspec --gen-bin printf).

Resolve command incompatibilities

Some commands have different options between BSD and GNU. If you handle the difference in the specfile, the test will be hard to read. You can solve it with the support command.

#!/bin/sh -e
# Command name: @sed
. "$SHELLSPEC_SUPPORT_BIN"
case $OSTYPE in
  *darwin*) invoke gsed "$@" ;;
  *) invoke sed "$@" ;;
esac

Tagging

The example groups or examples can be tagged, and the --tag option can be used to filter the examples to be run. The tag name and tag value are separated by :, and the tag value is optional. You can use any character if quoted.

Describe "Checking something" someTag:someVal
  It "does foo" tagA:val1
    ...
  It "does bar" tagA:val2
    ...
  It "does baz" tagA
    ...
End
  1. Everything nested inside a selected element is selected in parent elements. e.g. --tag someTag will select everything above.
  2. Specifying a tag but no value selects everything with that tag whether or not it has a value, e.g. --tag tagA will select everything above.
  3. Specifying multiple tags will select the union of everything tagged, e.g. --tag tagA:val1,tagA:val2 will select does foo and does bar.
  4. Tests included multiple times are not a problem, e.g. --tag someTag,tagA,tagA:val1 just selects everything.
  5. If no tag matches, nothing will be run, e.g. --tag tagA: runs nothing (it does not match baz above, as empty values are not the same as no value).
  6. The --tag option can be used multiple times, e.g. --tag tagA:val1 --tag tagA:val2 works the same as --tag tagA:val1,tagA:val2

About testing external commands

ShellSpec is a testing framework for shell scripts, but it can be used to test anything that can be executed as an external command, even if it is written in another language. Even shell scripts can be tested as external commands.

If you are testing a shell script as an external command, please note the following.

  • It will be executed in the shell specified by the shebang not the shell running the specfile.
  • The coverage of the shell script will not be measured.
  • Cannot refer to variables inside the shell script.
  • Shell built-in commands cannot be mocked.
  • Functions defined inside the shell script cannot be mocked.
  • Only command-based mock can be used (if the script is calling an external command).
  • Interceptor is not available.

To get around these limitations, use run script or run source. See How to test a single file shell script.

How to test a single file shell script

If the shell script consists of a single file, unit testing becomes difficult. However, there are many such shell scripts.

ShellSpec has the ability to testing in such cases with only few modifications to the shell script.

Using run script

Unlike the case of executing as an external command, it has the following features.

  • It will run in the same shell (but another process) that is running specfile.
  • The coverage of the shell script will be measured.

There are limitations as follows.

  • Cannot refer to variables inside the shell script.
  • Shell built-in commands cannot be mocked.
  • Functions defined inside the shell script cannot be mocked.
  • Only command-based mock can be used (if the script is calling an external command).
  • Interceptor is not available.

Using run source

It is even less limitations than run script and has the following features.

  • It will run in the same shell and same process that is running specfile.
  • The coverage of the shell script will be measured.
  • Can be refer to variables inside the shell script.
  • Function-based mock and command-based mock are available.
  • Interceptor is available.
  • Shell built-in commands can be mocked.
  • Functions defined inside the shell script can be mocked using interceptor.

However, since it is simulated using the . command, there are some differences in behavior. For example, the value of $0 is different.

NOTE: Mocking of shell built-in commands can be done before run source. However, if you are using interceptor, mocking of the test command must be done in the __<name>__ function.

Testing shell functions

__SOURCED__

This is the way to test shell functions defined in a shell script.

Loading a script with Include defines a __SOURCED__ variable available in the sourced script. If the variable __SOURCED__ is defined, please return from the shell script.

#!/bin/sh
# hello.sh

hello() { echo "Hello $1"; }

# This is the writing style presented by ShellSpec, which is short but unfamiliar.
# Note that it returns the current exit status (could be non-zero).
${__SOURCED__:+return}

# The above means the same as below.
# ${__SOURCED__:+x} && return $?

# If you don't like the coding style, you can use the general writing style.
# if [ "${__SOURCED__:+x}" ]; then
#   return 0
# fi

hello "$1"
Describe "hello.sh"
  Include "./hello.sh"

  Describe "hello()"
    It "says hello"
      When call hello world
      The output should eq "Hello world"
    End
  End
End

Intercepting

Interceptor is a feature that allows you to intercept your shell script in the middle of its execution. This makes it possible to mock functions that cannot be mocked in advance at arbitrary timing, and to make assertions by retrieving the state of during script execution.

It is a powerful feature, but avoid using it as possible, because it requires you to modify your code and may reduce readability. Normally, it is not a good idea to modify the code just for testing, but in some cases, there is no choice but to use this.

#!/bin/sh
# ./today.sh

# When run directly without testing, the "__()" function does nothing.
test || __() { :; }

# the "now()" function is defined here, so it can't be mocked in advance.
now() { date +"%Y-%m-%d %H:%M:%S"; }

# The function you want to test
today() {
  now=$(now)
  echo "${now% *}"
}

# I want to mock the "now()" function here.
__ begin __

today=$(today)
echo "Today is $today"

__ end __
Describe "today.sh"
  Intercept begin
  __begin__() {
    now() { echo "2021-01-01 01:02:03"; }
  }
  __end__() {
    # The "run source" is run in a subshell, so you need to use "%preserve"
    # to preserve variables
    %preserve today
  }

  It "gets today's date"
    When run source ./today.sh
    The output should eq "Today is 2021-01-01"
    The variable today should eq "2021-01-01"
  End
End

Intercept

Usage: Intercept [<name>...]

Specify the name(s) to intercept.

NOTE: I will change Intercept to Interceptors to make it a declarative DSL.

test || __() { :; }

Define the __ function that does nothing except when run as a test (via ShellSpec). This allows you to run it as a production without changing the code.

The test command is the shell built-in test command. This command returns false (non-zero) when called with no arguments. This will allow who are not familiar with ShellSpec to will understand what the result will be, even if they don't know what the code is for. Of course, it is good practice to comment on what the code is for

When run via ShellSpec, the test command is redefined and returns true "only once" when called with no arguments. After that, it will return to its original behavior. This means that this code needs to be executed only once, at the start of the shell script.

__

Usage: __ <name> [arguments...] __

This is where the process is intercepted. You can define more than one. If the name matches the name specified in Intercept, the __<name>__ function will be called.

Note that if the name is not specified in Intercept, nothing will be done, but the exit status will be changed to 0.

spec_helper

The spec_helper can be used to set shell options for all specfiles, define global functions,check the execution shell, load custom matchers, etc.

The spec_helper is the default module name. It can be changed to any other name, and multiple modules can be used. Only characters accepted by POSIX as identifiers can be used in module names. The file name of the module must be the module name with the extension .sh appended. It is loaded from SHELLSPEC_LOAD_PATH using the --require option.

The following is a typical spec_helper. The following three callback functions are available.

# Filename: spec/spec_helper.sh

set -eu

spec_helper_precheck() {
  minimum_version "0.28.0"
  if [ "$SHELL_TYPE" != "bash" ]; then
    abort "Only bash is supported."
  fi
}

spec_helper_loaded() {
  : # In most cases, you won't use it.
}

spec_helper_configure() {
  import 'support/custom_matcher'
  before_each "global_before_each_hook"
}

# User-defined global function
global_before_each_hook() {
  :
}

# In version <= 0.27.x, only shellspec_spec_helper_configure was available.
# This callback function is still supported but deprecated in the future.
# Please rename it to spec_helper_configure.
# shellspec_spec_helper_configure() {
#  :
# }

The spec_helper will be loaded at least twice. The first time is at precheck phase, which is executed in a separate process before the specfile execution. The second time will be load at the beginning of the specfile execution. If you are using parallel execution, it will be loaded every specfile.

Within each callback function, there are several helper functions available. These functions are not available outside of the callback function. Also, these callback functions will be removed automatically when spec_helper is finished loading. (User-defined functions will not be removed.)

<module>_precheck

This callback function will be invoked only once before loading specfiles. Exit with exit or abort, or return non-zero to exit without executing specfiles. Inside this function, set -eu is executed, so an explicit return on error is not necessary.

Since it is invoked in a separate process from specfiles, changes made in this function will not be affected in specfiles.

minimum_version

  • Usage: minimum_version <version>

Specifies the minimum version of ShellSpec that the specfile supports. The version format is semantic version. Pre-release versions have a lower precedence than the associated normal version, but comparison between pre-release versions is not supported. The build metadata will simply be ignored.

NOTE: Since <module>_precheck is only available in 0.28.0 or later, it can be executed with earlier ShellSpecs even if minimum_version is specified. To avoid this, you can implement a workaround using --env-from.

# spec/env.sh
# Add `--env-from spec/env.sh` to `.shellspec`
major_minor=${SHELLSPEC_VERSION%".${SHELLSPEC_VERSION#*.*.}"}
if [ "${major_minor%.*}" -eq 0 ] && [ "${major_minor#*.}" -lt 28 ]; then
  echo "ShellSpec version 0.28.0 or higher is required." >&2
  exit 1
fi

error, warn, info

  • Usage: error [messages...]
  • Usage: warn [messages...]
  • Usage: info [messages...]

Outputs a message according to the type. You can also use echo or printf.

abort

  • Usage: abort [messages...]
  • Usage: abort <exit status> [messages...]

Display an error message and exit. If the exit status is omitted, it is 1. You can also exit with exit. exit 0 will exit normally without executing the specfiles.

setenv, unsetenv

  • Usage: setenv [name=value...]
  • Usage: unset [name...]

You can use setenv or unsetenv to pass or remove environment variables from precheck to specfiles.

environment variables

The following environment variables are defined.

  • VERSION - ShellSpec Version
  • SHELL_TYPE - Currently running shell type (e.g. bash)
  • SHELL_VERSION - Currently running shell version (e.g. 4.4.20(1)-release)

NOTE: Be careful not to confuse SHELL_TYPE with the environment variable SHELL. The environment variable SHELL is the user login shell, not the currently running shell. It is a variable set by the system, and which unrelated to ShellSpec.

<module>_loaded

It is called after loading the shellspec's general internal functions, but before loading the core modules (subject, modifier, matcher, etc). If parallel execution is enabled, it may be called multiple times in isolated processes. Internal functions starting with shellspec_ can also be used, but be aware that they may change.

This was created to perform workarounds for specific shells in order to test ShellSpec itself. Other than that, I have not come up with a case where this is absolutely necessary, but if you have one, please let me know.

<module>_configure

This callback function will be called after core modules (subject, modifier, matcher, etc) has been loaded. If parallel execution is enabled, it may be called multiple times in isolated processes. Internal functions starting with shellspec_ can also be used, but be aware that they may change. It can be used to set global hooks, load custom matchers, etc., and override core module functions.

import

  • Usage: import <module> [arguments...]

Import a custom module from SHELLSPEC_LOAD_PATH.

before_each, after_each

  • Usage: before_each [hooks...]
  • Usage: after_each [hooks...]

Register hooks to be executed before and after every example. It is the same as executing BeforeEach/AfterEach at the top of all specfiles.

before_all, after_all

  • Usage: before_all [hooks...]
  • Usage: after_all [hooks...]

Register hooks to be executed before and after all example. It is the same as executing BeforeAll/AfterAll at the top of all specfiles.

NOTE: This is a hook that is called before and after each specfile, not before and after all specfiles.

Self-executable specfile

Add eval "$(shellspec - -c) exit 1" to the top of the specfile and give execute permission to the specfile. You can use /bin/sh, /usr/bin/env bash, etc. for shebang. The specfile will be run in the shell written in shebang.

#!/bin/sh

eval "$(shellspec - -c) exit 1"

# Use the following if version <= 0.27.x
# eval "$(shellspec -)"

Describe "bc command"
  bc() { echo "$@" | command bc; }

  It "performs addition"
    When call bc "2+3"
    The output should eq 5
  End
End

The -c option is available since 0.28.0, and you can also pass other options. If you run the specfile directly, --pattern will be automatically set to *. These options will be ignored if run via shellspec command.

The use of shellspec as shebang is deprecated because it is not portable.

#!/usr/bin/env shellspec -c
Linux does not allow passing options

#!/usr/bin/env -S shellspec -c
The -S option requires GNU Core Utilities 8.30 (2018-07-01) or later.

Use with Docker

You can run ShellSpec without installation using Docker. ShellSpec and specfiles run in a Docker container.

See How to use ShellSpec with Docker.

Extension

Custom subject, modifier and matcher

You can create custom subject, custom modifier and custom matcher.

See examples/spec/support/custom_matcher.sh for custom matcher.

NOTE: If you want to verify using shell function, you can use result modifier or satisfy matcher. You don't need to create a custom matcher, etc.

Code Coverage

ShellSpec has integrated coverage feature. To use this feature Kcov (v35 or later) is required.

Supported shells

Supported only in bash, zsh, and ksh, where DEBUG trap is implemented. However, we recommend latest bash for the following reasons.

  • bash (older versions): There seems to be some code that cannot be measured correctly.
  • zsh: There seems to be some code that cannot be measured correctly, and the measurement rate will be the lowest.
  • ksh: ksh93u+ and ksh2020 may have side effects on exit status when DEBUG trap is enabled due to a bug. This bug has been fixed in ksh93u+m. These are also prone to instability, especially with ksh2020 (which has been abandoned).

In any shell, some code may not be measured correctly (e.g., code containing eval or newline). These are limitations and problems caused by shells and Kcov.

Measurement target

ShellSpec measures only the necessary codes to improve the measurement speed. Also, there are some things that cannot be measured due to implementation.

  • The shell scripts loaded by Include will be measured.
  • The shell functions called by the When evaluation will be measured.
  • The shell scripts executed by the When run script evaluation will be measured.
  • The shell scripts executed by the When run source evaluation will be measured.
  • The external commands executed by the When evaluation will NOT be measured.
    • Even if it is a shell script, it is not measured when it is executed as an external command.
  • If other than the above, it will not be measured.

By default only shell scripts whose names contain .sh are coverage targeted. If you want to include other files, you need to adjust options with --kcov-options.

# Default kcov (coverage) options
--kcov-options "--include-path=. --path-strip-level=1"
--kcov-options "--include-pattern=.sh"
--kcov-options "--exclude-pattern=/.shellspec,/spec/,/coverage/,/report/"

# Example: Include script "myprog" with no extension
--kcov-options "--include-pattern=.sh,myprog"

# Example: Only specified files/directories
--kcov-options "--include-pattern=myprog,/lib/"

Coverage report

[Coverage report][coverage] and cobertura.xml and sonarqube.xml files are generated under the coverage directory by Kcov. You can easily integrate with Coveralls, Code Climate, Codecov, etc.

For developers

Subprojects

ShellMetrics - Cyclomatic Complexity Analyzer for shell scripts

URL: https://github.com/shellspec/shellmetrics

ShellBench - A benchmark utility for POSIX shell comparison

URL: https://github.com/shellspec/shellbench

altshfmt - AltSH (alternative shell script) formatter

URL: https://github.com/shellspec/altshfmt

Related projects

getoptions - An elegant option parser and generator for shell scripts

URL: https://github.com/ko1nksm/getoptions

readlinkf - readlink -f implementation for shell scripts

URL: https://github.com/ko1nksm/readlinkf

portable-echo - Portable echo shell function for POSIX compliant shells

URL: https://github.com/ko1nksm/portable-echo.sh

Inspired frameworks

  • RSpec - Behaviour Driven Development for Ruby
  • Jest - Delightful JavaScript Testing
  • Mocha - the fun, simple, flexible JavaScript test framework
  • Jasmine - Behavior-Driven JavaScript
  • Ginkgo - A Golang BDD Testing Framework
  • JUnit 5 - The programmer-friendly testing framework for Java

Contributions

All contributions are welcome!

ShellSpec uses a peculiar coding style to assure high performance, reliability and portability, and the external commands allowed to use are greatly restricted.

We recommend that you create WIP PR early or offer suggestions in discussions to avoid ruining your work.

See CONTRIBUTING.md

shellspec's People

Contributors

antonimarcinek avatar damienflament avatar eltociear avatar funtimecoding avatar inokappa avatar kdnakt avatar ko1nksm avatar ldicarlo avatar leonstafford avatar nreilingh avatar robert-gurol-signavio avatar rowanthorpe avatar somasis avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

shellspec's Issues

Redirecting strings to stdin with <<< to test `read -p` works locally but not in Travis VM

I'm trying to write the spec of a checking of local Docker.io installation. This proposes the installation of Docker if the user types y in his shell. As the checking is based on command -v docker, I change the $PATH to do not provide access to docker command and simulate its non-installation.

Here is my spec

    It "checks that docker.io is installed and do it"
        mkdir -p spec/tmp_bin
        ln -s -f "$(command -v dirname)" spec/tmp_bin/dirname
        ln -s -f "$(command -v readlink)" spec/tmp_bin/readlink
        ln -s -f "$(command -v grep)" spec/tmp_bin/grep
        # do not add sudo to let the installation fail during tests
        BeforeRun "export PATH='spec/tmp_bin'"
        # BeforeRun "export IFS='.'"
        When run source ./php -v <<< "y"
        # When run source ./php -v
        The line 1 of stdout should eq "docker not installed"
        The line 2 of stdout should eq "sudo apt-get install docker.io"
        The line 3 of stdout should eq "Unable to launch docker installation. Please do it manually."
        The stderr should eq ""
        rm -rf spec/tmp_bin
    End

This works perfectly locally but on a Travis VM it seems that read never receives the y: https://travis-ci.org/jclaveau/docker-php-multiversion/builds/632947959

I tried to change the IFS without success, I added a read <<< "xxx" in the before_script to test it and it works:

$ read -p "test? " -n 6 -r <<< "coucou"; echo ""; echo "$REPLY"

coucou

So I really do not know what to do in this case and if something can be done on the shellspec side.

Thanks in advance,
Jean

The function called by run evaluation stops with an error in the middle when errexit is off by default

No problem if specified with Set 'errexit: off'

func() { printf foo; false; printf bar; }

Context 'errexit is off'
  Set 'errexit:off'

  # Success as expected
  Specify "The func() does not stop with 'false' with run evaluation"
    When run func
    The stdout should eq 'foobar'
    The status should be success
  End

  # Success as expected
  Specify "The func() does not stop with 'false' with call evaluation"
    When call func
    The stdout should eq 'foobar'
    The status should be success
  End
End

Context 'errexit is on'
  Set 'errexit:on'

  # Success as expected
  Specify "The func() stops with 'false' with run evaluation"
    When run func
    The stdout should eq 'foo'
    The status should be failure
  End

  # Success as expected (Impossible to catch an error without subshell)
  Specify "The func() does NOT stop with 'false' with call evaluation"
    When call func
    The stdout should eq 'foobar'
    The status should be success
  End
End

Incorrect if specified with set +e (default) in spec_helper.sh

# spec/spec_helper.sh
set +e # default
func() { printf foo; false; printf bar; }

Context 'errexit is off'
  # Fail
  Specify "The func() does not stop with 'false' with run evaluation"
    When run func
    The stdout should eq 'foobar'
    The status should be success
  End

  # Success as expected
  Specify "The func() does not stop with 'false' with call evaluation"
    When call func
    The stdout should eq 'foobar'
    The status should be success
  End
End

This problem plan to fixed by next release, but it is a BREAKING CHANGE.

The workaround for this issue is to specify it explicitly with Set 'errexit:off'.

Run only failed examples (instead of --only-failures and --next-failure)

rspec has --only-failures and --next-failure to filter to execute the examples that failed the last time.

This feature requires to prepare config.example_status_persistence_file_path. IMO, I do not think that it is easy to use.

I think it would be convenient if there is an interactive mode (list failed examples, re-run failed examples, run specifled example, etc)

'When run source' breaks script_dir=$(dirname "$(readlink -f "$0")")

I use script_dir=$(dirname "$(readlink -f "$0")") before source "$script_dir/../lib/functions.bash".

It works perfectly when I call my script but script_dir as the wrong value when called from shellspec: the pwd of shellspec. In other words: /.../my_lib instead of /.../my_lib/bin.

Effectively, in my case, $0 values /home/jean/dev/docker-php-multiversion/docker-php-multiversion [specfiles]

Presently, the workaround I found is the following:

script_dir=$(dirname "$(readlink -f "$0")")
if [ -d "$script_dir"/bin ]; then
    script_dir="$script_dir"/bin
fi

Dockerhub

First, is this you: https://hub.docker.com/u/shellspec; if case, common man, it's as if you're not even trying.

Now stay with me, but you have developed a framework, to run BDD tests, which would (should) be part of an orchestration pipeline, and yet it seems that dog food has not been included; ie, a deployment pipeline, triggered by events on github, that, uhhh, runs tests, builds artifacts and updates the above dockerhub registry.

If you do decide to pay more attention to the docker side, please use alpine; btw, thank you so godamn much for adhering to posix compliance. :)

Improve documentation (and english) quality

I am not a native English speaker, so I think some sentences are difficult to understand. Feel free to make a pull request if you have suggestions for improving the documentation. Documentation-only fixes are also very welcome.

command-based mock

Current shellspec support function-based mock. Using function-based mock is recommended for performance. However command-based mock can implement feature that cannot with function based mock.

e.g

  • Give a command name invalid as the function name.
  • Search the command from PATH.
  • Un-mock / Re-mock during evaluation.

Provide example of how to use multiple spec files

Instead of having all my tests in a single large spec file, I would like to be able to use multiple spec files that are referenced from a master spec file. I could not find an example of how to do this. Is this possible? If so, please provide an example. If not, this is a feature request.

The example after a failed example is run even with --jobs 0 and --fail-fast=1

I'm seeing that an example after a failed example get run even with the --jobs 0 --fail-fast=1 options for some reason. The report from shellspec doesn't show it but I'm sure the example after the failed example is run because I could see its side effects.

I also can't reproduce this in another test project with simple examples so far. But in my current project it seems to happen consistently. I'll see if I can reproduce it with simple examples and report back here. Thanks!

Data helper don't expand variables from Parameters helper

When using the Data helper to provide input to the tested program within a parameterized test, the variables provided by the Parameters helper (i.e. the positional parameters $1, $2, etc.) are unknown to the Data helper.

Specfile to reproduce the bug

#shellcheck shell=bash

Describe "Data helper"
    Parameters
        "foo"
    End

    Describe "block style"
        Describe ":expand"
            It "expands the variables from Parameters helper"
                Data:expand
                    #|$1
                End

                When call cat
                The output should equal "$1"
            End

            It "expands the variables"
                # shellcheck disable=SC2034
                input=$1

                Data:expand
                    #|$input
                End

                When call cat
                The output should equal "$1"
            End
        End
    End
End

Error message

$ ./shellspec -f d spec/bugs_spec.sh
Running: /bin/sh [bash 5.0.16(1)-release]

Data helper
  block style
    :expand
      expands the variables from Parameters helper (FAILED - 1)
      expands the variables

Examples:
  1) Data helper block style :expand expands the variables from Parameters helper
     call cat

     1.1) The output should equal foo

            expected: "foo"
                 got: ""
            
          # spec/bugs_spec.sh:16

     1.2) [WARNING] It was output to stderr but not found expectation

            stderr: main: line 53: $1: unbound variable
            
          # spec/bugs_spec.sh:10

Finished in ? seconds (user ? seconds, sys ? seconds)
2 examples, 1 failure


Failure examples: (Listed here affect your suite's status)

shellspec spec/bugs_spec.sh:10 # 1) Data helper block style :expand expands the variables from Parameters helper FAILED

The parameter is seen as unbound within the Data helper.

As you can see within the second example, a workaround is to assign the value of the parameter to another variable which is expanded in the Data helper.

Add trace option

Display the command to be executed like set -x or set -v for debugging.

This feature is not trace of the specfile but trace executed command within the evaluation.

Shellspec fails when using `nounset` option

I use Bash strict mode in my script and unit tests. When an unset parameter is expanded in my tested script (due to a bug), this cause an error in Shellspec core.

I added an example in the Shellspec specification to bring out this bug:

Context "When using Bash strict mode"
    Skip if "not on Bash" test "${BASH:-}"x = x

    set -o nounset

    fDescribe "Shellspec"
        It "runs an example which expands an unset parameter"
            expand_unset() {
                : "${UNSET_PARAMETER}"
            }

            When call expand_unset
            The status should be failure
        End
    End
End

The expected result would be:

$ shellspec --focus
Running: /bin/sh [bash 5.0.11(1)-release]
F

Examples:
  1) When using Bash strict mode Shellspec runs an example which expands an unset parameter
     1.1) Example aborted (exit status: 1)

            stderr:main: line 93: UNSET_PARAMETER: unbound variable
            
          # spec/bugs_spec.sh:4

Finished in ? seconds (user ? seconds, sys ? seconds)
1 example, 1 failure

Failure examples: (Listed here affect your suite's status)

shellspec spec/bugs_spec.sh:30 # 1) When using Bash strict mode Shellspec runs an example which expands an unset parameter FAILED

But it is:

$ shellspec --focus
Running: /bin/sh [bash 5.0.11(1)-release]
/usr/lib/shellspec/lib/core/outputs.sh: line 176: LF: unbound variable
Unexpected output to the stderr at line 29-37 in 'spec/bugs_spec.sh'


Finished in ? seconds (user ? seconds, sys ? seconds)
0 examples, 0 failures, aborted by an unexpected error

An unexpected error occurred or output to the stderr. [0] [1] [102]
Fatal error occurred, terminated with exit status 1.

And with debug output:

image

But using the current Shellspec specification does not bring up the bug, because the LF parameter is defined in the specification helper:

LF="$SHELLSPEC_LF" TAB="$SHELLSPEC_TAB"

Because this parameter is not defined in my specification helper, it causes the error.

AIX (7.2.1.4 and bash 4.3.30) is supported also

Hello,
nice tool, works well with AIX 7.2.1.4 and bash 4.3.30.
The shellspec 0.20.2 tool pass the most of your tests, but skip some tests, see below.
I use shellspec to test my scripts for several months, everythings seems to work well, I have no problems.

With regards,
Lutz

Appendage:
The list of the skipped tests (shellspec 0.20.2).

shellspec spec/core/matchers/be/stat_spec.sh:89 # 1) core/matchers/be/stat.sh be symlink matcher example SKIPPED
shellspec spec/core/matchers/be/stat_spec.sh:116 # 2) core/matchers/be/stat.sh be pipe matcher example SKIPPED
shellspec spec/core/matchers/be/stat_spec.sh:143 # 3) core/matchers/be/stat.sh be socket matcher example SKIPPED
shellspec spec/core/matchers/be/stat_spec.sh:170 # 4) core/matchers/be/stat.sh be readable matcher example SKIPPED
shellspec spec/core/matchers/be/stat_spec.sh:198 # 5) core/matchers/be/stat.sh be writable matcher example SKIPPED
shellspec spec/core/matchers/be/stat_spec.sh:226 # 6) core/matchers/be/stat.sh be executable matcher example SKIPPED
shellspec spec/core/matchers/be/stat_spec.sh:253 # 7) core/matchers/be/stat.sh be block device matcher example SKIPPED
shellspec spec/core/matchers/be/stat_spec.sh:280 # 8) core/matchers/be/stat.sh be charactor device matcher example SKIPPED
shellspec spec/core/matchers/has/stat_spec.sh:13 # 9) core/matchers/has/stat.sh has setgid matcher example SKIPPED
shellspec spec/core/matchers/has/stat_spec.sh:41 # 10) core/matchers/has/stat.sh has setuid matcher example SKIPPED

Allow using status as a modifier for the function subject?

Currently, I have to write: The result of function assert_abc should equal OK and then in my assert_abc function I'll output OK on success.

It would be nice if I can simply write: The status of function assert_abc should be success, then I just need to make assert_abc return a non-zero status on success.

1 example, 0 failures, aborted by an unexpected error

Hey!
I'm trying to test a little bash executable file calling php from a docker image:

#!/bin/bash

version=7.3

while :; do
    case $1 in
        5.6|7.0|7.1|7.2|7.3|7.4|latest)       # Takes an option argument; ensure it has been specified.
            version=$1
            ;;
        *)               # Default case: No more options, so break out of the loop.
            break
    esac

    shift
done

if [[ -f /.dockerenv ]] || grep -Eq '(lxc|docker)' /proc/1/cgroup; then 
        # we do not need to restart a container with php multiversion
        php$version "$@"
    else 
    
        docker_path=$(command -v docker)
        if [ -z "$docker_path" ]; then
            # echo "docker not installed"
            echo $docker_path
            read -p "Do you want to install docker? " -n 1 -r
            # echo    # (optional) move to a new line
            if [[ $REPLY =~ ^[Yy]$ ]]
            then
                sudo apt-get install docker
            fi
        fi
        
        echo "Docker: php$version $@"
        
        # avoid https://stackoverflow.com/questions/43099116/error-the-input-device-is-not-a-tty
        test -t 1 && USE_TTY="-t" 
        
        docker run -i ${USE_TTY} --rm \
            -v $(pwd):$(pwd) \
            -w $(pwd) \
            --user $(id -u):$(id -g) \
            --volume="$HOME:$HOME:rw" \
            --volume="/etc/group:/etc/group:ro" \
            --volume="/etc/passwd:/etc/passwd:ro" \
            --volume="/etc/shadow:/etc/shadow:ro" \
            --volume="/etc/sudoers.d:/etc/sudoers.d:ro" \
            jclaveau/php-multiversion php$version "$@"
        
fi

Here is my spec file

Describe "Sample specfile"
  Describe "php"
    It "runs php in 5.6"
      When run source ./php 5.6 spec/phpversion.php
      The line 1 of stdout should eq "Docker: php5.6 spec/phpversion.php"
      The line 2 of stdout should eq "5.6"
      The stderr should eq ""
    End
    It "runs php in 7.0"
      When run source ./php 7.0 spec/phpversion.php
      The line 1 of stdout should eq "Docker: php7.0 spec/phpversion.php"
      The line 2 of stdout should eq "7.0"
      The stderr should eq ""
    End
  End
  
End

spec/phpversion.php simply prints the Major/Minor part of the running php version.

To test it with your (f***ing great test framework :) ) i use shellspec --shell bash (running with /bin/sh doesn't work due to [[ statements and the #!/bin/bash is not handled by your runner automatically, let me know if you want me to open a dedicated bug)

And I get the following error which I'm unable to get rid of:

shellspec --shell bash 
Running: /usr/bin/bash [bash 5.0.3(1)-release]
..

Finished in 2.18 seconds (user 0.28 seconds, sys 0.13 seconds)
2 examples, 0 failures, aborted by an unexpected error

An unexpected error occurred or output to the stderr. [0] [1] [0]
Fatal error occurred, terminated with exit status 1.

Obviously, when I run the comment outside your framework, it works perfectly:

./php 5.6 spec/phpversion.php 
Docker: php5.6 spec/phpversion.php
5.6

Is there anything I missed or is it due to something I'm not able to debug in your framework?

Thanks in advance and for your impressive work,
Jean

test errors, assert exit status

Hi!

Thanks for the awesome framework! I really like the syntax, because it's really easy to read, and it can be also extended with simple bash statements, so it does not feel like a limitation.

Maybe I just missed it in the guide, but I did not find a way to easily test error handling and assert exit status.

E.g., greet.sh should print if it's given a parameter, but it should fail if it's called without without an argument, and print help.

So far I was only able to test the happy path, because the test-case terminates prematurely with the following warning if my script terminates with a non-zero exit status:

[WARNING] It was exits with status non-zero but not found expectation

            status: 1

Also, I found no constructs in ShelSpec's language to assert stderr, and test exit status codes. Is it possible or planned to support these test cases?

Thank you,
David

Focus on a specific example or an example group

This unit testing framework is awesome, thanks a lot for your hard work !

I searched throught documentation and I haven't found a way to focus on a specific example or example group like we can dot with fit or fdescribe in other unit testing framework.
It's pretty handy when writing tests to not run the whole suite.

Did I miss the functionality or it could be implemented ?

`Assert` expectation for custom assertion

ShellSpec already has result of modifier for custom assertion. It can be assert using custom function. However, it can not pass arguments and you might want a simpler custom assertion.

Here are sample.

exists_docker_image() {
  docker inspect "$1" >/dev/null 2>&1
}

It "builds docker image"
  When run create_docker_image my_image
  Assert exists_docker_image my_image
End

Planned specifications

Difference between result of and Assert.

The result of is

  • custom assertion for result of evaluation.
    • That is, the arguments of the custom function are stdout, stderr, and status.

The Assert is

  • custom assertion for external side effects.
    • That is, the arguments of the custom function are passed as is.
  • You can not assert stdout/stderr/status of evaluation.
    • To be exact, it is possible by using the variable SHELLSPEC_STDOUT etc, But not recommend because it is internal variable.
  • Enable errexit before calling custom function.
    • To make it easy to assert without a return like bats.

NOTE: No plan to implement AssertEqual etc. Because it can be implemented with Assert.

Do you have any suggestion? or thumbs up if you like it.

For loop example

Hi,
I would like to execute a test case whose logic remains same hence I don't want to write duplicate code for evaluation and expectation. Hence I want to evaluate when and expectation in a for loop by passing bunch of arguments.

Sample code:

Describe "Sample specfile"

  Describe "hello()"
    hello() {
      echo  "hello $1"
    }

      It "puts greeting"
        for i in $arg1 $arg2
        do
          When call hello $i
          The output should eq "hello world"
        done
      End 

  End

End

Whenever I execute the code I get the following the second evaluation fails stating

Evaluation has been already executed

. Below is the terminal output when I execute. Shellspec version used 0.22.0.

shellspec spec/shellspec_spec.sh --env arg1=world --env arg2=universe

Running: /bin/sh [bash 3.2.57(1)-release]
F

Examples:
  1) Sample specfile hello() puts greeting
     call hello world

     1.1) call hello universe

            Evaluation has already been executed
            
          # spec/shellspec_spec.sh:10

Finished in 0.09 seconds (user 0.08 seconds, sys 0.02 seconds)
1 example, 1 failure

Failure examples: (Listed here affect your suite's status)

shellspec spec/shellspec_spec.sh:7 # 1) Sample specfile hello() puts greeting FAILED

Could you please let me know if this is even possible in shell spec? If possible then how can be this implemented? Any help will be highly appreciated.

Thanks,
ark

Coverage support for zsh and ksh

Kcov only supports bash, however it may work if I can simulate the same output as bash. I'm not sure if it is possible, but it's worth trying.

Improve pattern matching

The pattern matching of shell scripts lacks functions such as repeating the same character. Regular expressions can be used with expr command etc., but I think about implement extended patterns with shell scripts.

This is code that created for a different purpose before. (I don't know if I use this code.)

scanner.sh

#!/bin/sh
# shellcheck disable=SC2016

scanner_error() {
  echo "scanner: $1" >&2
  exit 1
}

scanner_begin() {
  locals='_name _buf _len _leave _i _max'
  echo "{"
  echo 'if eval "scan(){ local _;};scan" 2>/dev/null; then'
  echo "  eval 'scan() { local $locals; _scan \"\$@\"; }'"
  echo 'else'
  echo "  eval 'function scan { typeset $locals; _scan \"\$@\"; }'"
  echo 'fi'
  echo '_scan() {'
  echo '  _name=$1 _buf=$2 _leave="" SCANNER_CAPTURE_LENGTH=0'
  echo '  _i=0'
  echo '  while [ $_i -le "$SCANNER_CAPTURE_LENGTH_MAX" ]; do'
  echo '    eval "unset SCANNER_CAPTURE$_i"'
  echo '    _i=$((_i+1))'
  echo '  done'
  echo '  "_${_name}_scanner" || return 1'
  echo '  [ "${3:-_}" = _ ] || eval "$3=\$_buf"'
  echo '  [ "${4:-_}" = _ ] || eval "$4=\${2"$_leave\$_buf"}"'
  echo '  _i=5'
  echo '  while [ $_i -le $# ]; do'
  echo '    eval "_name=\${$_i}"'
  echo '    eval "$_name=\${SCANNER_CAPTURE$((_i-4)):-}"'
  echo '    _i=$((_i+1))'
  echo '  done'
  echo '}'
  # shellcheck disable=SC1003,SC2116
  [ "$(echo '\\')" = '\' ] && ECHO_ESCAPE='\\' || ECHO_ESCAPE=''
  for i in Aa Bb Cc Dd Ee Ff Gg Hh Ii Jj Kk Ll Mm \
           Nn Oo Pp Qq Rr Ss Tt Uu Vv Ww Xx Yy Zz; do
    j=${i#?} i=${i%?}
    eval "IC_$i=[$i$j] IC_$j=[$i$j]"
  done

  IFS_ORIG=$IFS IFS=,
  set -- \
    'alnum=0-9A-Za-z' \
    'alpha=A-Za-z' \
    'ascii=\001-\177' \
    'blank=\011\040' \
    'cntrl=\001-\037\177' \
    'digit=0-9' \
    'graph=\041-\176' \
    'lower=a-z' \
    'print=\040-\176' \
    'punct=\041-\057\072-\100\133-\140\173-\176' \
    'space=\011-\015\040' \
    'upper=A-Z' \
    'word=0-9A-Z_a-z' \
    'xdigit=0-9A-Fa-f'

  # shellcheck disable=SC2059
  for cc in $(printf "$*"); do
    eval "CC_${cc%=*}=\${cc#*=}"
  done
  IFS=$IFS_ORIG
  SCANNER_DEFINES_LENGTH=0
  SCANNER_CAPTURE_LENGTH=0 SCANNER_CAPTURE_LENGTH_MAX=0
}

scanner_end() {
  echo "SCANNER_CAPTURE_LENGTH_MAX=$SCANNER_CAPTURE_LENGTH_MAX"
  echo "}"
}

scanner() {
  name=$1 pattern=$2 switch=${3:-}
  buf='' buf2='' ch='' mark='' ignorecase='' index='' lastindex=0
  shift='ch=${pattern%"${pattern#?}"} pattern=${pattern#?}'
  case $switch in
    *i*) ignorecase='$IC_'
  esac
  eval "$shift"
  while [ "$ch" ]; do
    case $ch in
      \[)
        buf2=$ch
        eval "$shift"
        while [ "$ch" ]; do
          case $ch$pattern in
            \[[:=.]*)
              eval "$shift"
              mark=$ch buf3=''
              eval "$shift"
              while [ "$ch" ]; do
                if [ "$ch" = "$mark" ]; then
                  case $mark in
                    :) eval "buf3=\${CC_$buf3:-[$mark$buf3$mark]}" ;;
                    *) buf3='['$mark$buf3$mark']'
                  esac
                  buf2="$buf2$buf3" pattern=${pattern#?}
                  break
                fi
                buf3=$buf3$ch
                eval "$shift"
              done
              eval "$shift"
              ;;
          esac
          case $ch in
            \])
              buf2=$buf2$ch
              case $pattern in
                \**) pattern=${pattern#?} end='*' ;;
                \+*) pattern=${pattern#?} end='*' buf=$buf$buf2 ;;
                \?*) pattern=${pattern#?} end='?' ;;
                \{*) pattern=${pattern#?} range=''
                  while :; do
                    eval "$shift"
                    [ "${ch:-\}}" = '}' ] && break
                    range=$range$ch
                  done
                  start=${range%%,*} end=${range#*,} i=0
                  while [ $((i=i+1)) -le $start ]; do
                    buf=$buf$buf2
                  done
                  [ "$end" ] && end=$((end-start)) || end='*'
                  ;;
                *)
                  buf=$buf$buf2
                  break
              esac
              scanner_define '=' "$buf"
              buf=''
              scanner_define "$end" "$buf2"
              break
              ;;
            \\) buf2=$buf2$ECHO_ESCAPE\\$ch ;;
            [\'\"]) buf2=$buf2\\$ch ;;
            *) buf2=$buf2$ch
          esac
          eval "$shift"
        done
        ;;
      \\)
        eval "$shift"
        case $ch in
          \\) buf=${buf:-}$ECHO_ESCAPE\\$ch ;;
          \[ | \( | \)) buf=${buf:-}\\$ch ;;
          *) buf=${buf:-}$ch
        esac
        ;;
      \()
        [ "$index" ] && scanner_error "$name already parentheses open"
        lastindex=$((lastindex+1)) index=$lastindex
        scanner_define '(' "$index"
        ;;
      \))
        [ "$index" ] || scanner_error "$name not parentheses open"
        scanner_define '=' "$buf"
        scanner_define ')' "$index"
        buf='' index=''
        ;;
      [\'\"*#$]) buf=${buf:-}\\$ch ;;
      [a-zA-Z]) eval 'buf=${buf:-}'$ignorecase$ch ;;
      *) buf=${buf:-}$ch
    esac
    eval "$shift"
  done
  scanner_define '=' "$buf"
  [ "$index" ] && scanner_error "$name parentheses not closed"
  scanner_generate "$name" "$switch" "$2"
}

scanner_define() {
  [ "$1" = 0 ] || [ -z "$2" ] && return 0
  eval "SCANNER_DEFINES_$SCANNER_DEFINES_LENGTH=\$1:\$2"
  SCANNER_DEFINES_LENGTH=$((SCANNER_DEFINES_LENGTH+1))
}

scanner_generate() {
  type='' param=''
  case $2 in
    *r*) i=$((SCANNER_DEFINES_LENGTH-1)) end=0 step=-1 del='%' leave='#';;
      *) i=0 end=$((SCANNER_DEFINES_LENGTH-1)) step=+1 del='#' leave='%';;
  esac
  echo "_$1_scanner() { # $3"
  echo "  _leave='$leave'"
  while :; do
    eval "type=\${SCANNER_DEFINES_$i%%:*}"
    eval "param=\${SCANNER_DEFINES_$i#*:}"

    case $type in
      =)
        echo '  _len=${#_buf} _buf=${_buf'"$del$param"'}'
        echo '  [ "$_len" -ne ${#_buf} ] || return 1'
        ;;
      \?)
        echo '  _buf=${_buf#'"$param"'}'
        ;;
      [0-9]*)
        echo '  _i=0'
        echo '  while [ $((_i=_i+1)) -le '"$type"' ]; do'
        echo '    _len=${#_buf} _buf=${_buf'"$del$param"'}'
        echo '    [ "$_len" -ne ${#_buf} ] || break'
        echo '  done'
        ;;
      \()
        echo '  SCANNER_CAPTURE'$param'=${_buf}'
        SCANNER_CAPTURE_LENGTH=$((SCANNER_CAPTURE_LENGTH+1))
        if [ $SCANNER_CAPTURE_LENGTH -gt $SCANNER_CAPTURE_LENGTH_MAX ]; then
          SCANNER_CAPTURE_LENGTH_MAX=$SCANNER_CAPTURE_LENGTH
        fi
        ;;
      \))
        echo '  SCANNER_CAPTURE'$param'=${SCANNER_CAPTURE'$param$leave'"$_buf"}'
        ;;
      \*)
        echo '  while :; do'
        echo '    _len=${#_buf} _buf=${_buf'"$del$param"'}'
        echo '    [ "$_len" -ne ${#_buf} ] || break'
        echo '  done'
        ;;
    esac
    eval "unset SCANNER_DEFINES_$i"

    [ "$i" -eq "$end" ] && break
    i=$((i+step))
  done
  if [ $SCANNER_CAPTURE_LENGTH -gt 0 ]; then
    echo "  # shellcheck disable=SC2034"
    echo "  SCANNER_CAPTURE_LENGTH=$SCANNER_CAPTURE_LENGTH"
  fi
  echo '}'
  SCANNER_DEFINES_LENGTH=0 SCANNER_CAPTURE_LENGTH=0
}

test.sh

#!/bin/sh

set -eu

main() {
  [ $# -eq 0 ] && run_tests
  if [ "$1" = -a ] || [ "$1" = --all ]; then
    set -- dash bash zsh ksh mksh yash posh 'busybox ash'
  fi
  for shell in "$@"; do
    if which "${shell%% *}" >/dev/null; then
      echo $shell "$0"
      $shell "$0"
    else
      echo "Skip: $shell not found"
    fi
  done
}

create_scanners() {
  . ./scanner.sh
  scanner_begin
  scanner number '[-+]?[1-9][0-9]*'
  scanner digit '[a[:digit:]]+'
  scanner hex '0x[0-9a-fA-F][0-9a-fA-F]'
  scanner long_option '--[0-9a-fA-F-]+[=]?'
  scanner test1 "a'b\)"
  scanner test2 '[a!]'
  scanner test3 '[a[=a=]]+'
  scanner test4 '[ab]{0,}'
  scanner test5 '[ab]{1,}'
  scanner test6 '[ab]{3,5}'
  scanner test7 '[abc]{2}'
  scanner test8 '\\100'
  scanner test9 '\[\]'
  scanner test10 '*'
  scanner test11 '"'
  scanner test12 "'"
  scanner test13 '$a'
  scanner test14 '#'
  scanner test15 '[a\]'
  scanner test16 '["]'
  scanner test17 'aBcD' i
  scanner test18 'aBcD'
  scanner comment '#[ ]+'
  scanner brace '\(\)'
  scanner capture '(aaa)([b]+)(c[d])'
  scanner capture2 '(aaa)[b]+c[d]'

  scanner blank '[[:blank:]]+'
  scanner space '[[:space:]]+'

  scanner reverse 'cd' r
  scanner reverse2 '[d]*e' r

  scanner section '[#]+[ ]{0,}[A-Z]+:'
  scanner_end
}

if [ "$(echo -n)" ]; then
  ok() { printf '.'; }
  ng() { printf ng; }
else
  ok() { echo -n '.'; }
  ng() { echo -n ng; }
fi

tests() {
  scan number -4123 && ok || ng
  scan digit a4123a && ok || ng
  scan hex 0x12 && ok || ng
  scan long_option "--aaa-bbb=" && ok || ng
  scan long_option "--aaa-bbb" && ok || ng
  scan long_option "--aaa-bbb=aaaa" && ok || ng
  scan long_option "--aaa-bbb aaaa" && ok || ng
  scan test1 "a'b)" && ok ||ng
  scan test2 '!' && ok || ng

  scan test4 'abab' && ok || ng
  scan test5 'abab' && ok || ng
  scan test5 '' && ng || ok
  scan test6 'ab' && ng || ok
  scan test6 'ababab' remain scanned && ok || ng
  [ "$scanned" = ababa ] && ok || ng
  [ "$remain" = b ] && ok || ng
  scan test7 'bababa' remain scanned && ok || ng
  [ "$scanned" = ba ] && ok || ng
  [ "$remain" = baba ] && ok || ng
  scan test8 '\100' && ok || ng
  scan test9 '[]' && ok || ng
  scan test10 '*' && ok || ng
  scan test11 '"' && ok || ng
  scan test12 "'" && ok || ng
  scan test13 '$a' && ok || ng
  scan test14 '#' && ok || ng
  scan test15 '\' && ok || ng
  scan test16 '"' && ok || ng
  scan test17 'ABcd' && ok || ng
  scan test18 'ABcd' && ng || ok

  scan test1 "zz" && ng || ok
  scan number "zz" && ng || ok

  scan comment "# test" && ok || ng
  scan brace "()" && ok || ng
  scan capture "aaabbbcd" _ _ a b c d && ok || ng
  [ "$a" = aaa ] && ok || ng
  [ "$b" = bbb ] && ok || ng
  [ "$c" = cd ] && ok || ng
  [ "$d" = "" ] && ok || ng

  scan capture "aaabbbcd" && ok || ng
  [ "$SCANNER_CAPTURE1" = aaa ] && ok || ng
  [ "$SCANNER_CAPTURE2" = bbb ] && ok || ng
  [ "$SCANNER_CAPTURE3" = cd ] && ok || ng
  [ "$SCANNER_CAPTURE_LENGTH" = 3 ] && ok || ng

  scan capture2 "aaabbbcd" _ _ a b c d && ok || ng
  [ "$a" = aaa ] && ok || ng
  [ "$b" = "" ] && ok || ng
  [ "$c" = "" ] && ok || ng

  scanned='' remain=''
  scan number -4123aaa remain scanned && ok || ng
  [ "$scanned" = -4123 ] && ok || ng
  [ "$remain" = aaa ] && ok || ng

  scan hex 0x1234 remain scanned && ok || ng
  [ "$scanned" = 0x12 ] && ok || ng
  [ "$remain" = 34 ] && ok || ng

  scan blank "$(printf '\011\040')" && ok || ng
  scan space " " && ok || ng

  scan reverse 'abcd' remain scanned && ok || ng
  [ "$scanned" = 'cd' ] && ok || ng
  [ "$remain" = 'ab' ] && ok || ng

  scan reverse2 'abcdddddde' remain scanned && ok || ng
  [ "$scanned" = 'dddddde' ] && ok || ng
  [ "$remain" = 'abc' ] && ok || ng

  scan section "# COMMAND:" && ok || ng

  return 0
}

run_tests() {
  eval "$(create_scanners)"
  tests
  echo
  exit
}

if [ $# -gt 0 ]; then
  main "$@"
else
  main
fi

Parameterized test

Syntax proposals

Describe 'example'
  Describe 'block style'
    Parameters
      "#1" 1 2 3
      "#2" 1 2 3
    End

    Example "example $1"
      When call echo "$(($2 + $3))"
      The output should eq "$4"
    End
  End

  Describe 'dynamic style'
    Parameters:dynamic
      for i in 1 2 3; do
        %data "#$i" 1 2 3
      done
    End

    Example "example $1"
      When call echo "$(($2 + $3))"
      The output should eq "$4"
    End
  End
End

Researches

SHELLSPEC_GROUP_ID: unbound variable

From Shellspec 0.24, my builds fail when running unit tests.

$ shellspec --no-banner spec/units

/home/travis/.local/lib/shellspec/lib/core/hook.sh: line 23: SHELLSPEC_GROUP_ID: unbound variable

Unexpected error (syntax error?) occurred in ''

Finished in 0.20 seconds (user 0.25 seconds, sys 0.02 seconds)
0 examples, 0 failures, aborted by an unexpected error

An unexpected error occurred. [executor: 1] [reporter: 1] [error handler: 102]
Fatal error occurred, terminated with exit status 1.

I ran a Travis build with Shellspec 0.23.0, 0.24.0, 0.24.1:
https://travis-ci.com/github/neimad/travis-python/builds/168011715

Self executing specfile.

I plan to implements self executing specfile.

Since shellspec is intended for unit tests, it is best practice to create a directory structure and execute by shellspec. However for simple test case like system test, It is useful to be able to execute the specification file itself.

shUnit2 and Bats-core are also can do the same.

shUnit2

#! /bin/sh
# file: examples/equality_test.sh

testEquality() {
  assertEquals 1 1
}

# Load shUnit2.
. ./shunit2
$ cd examples
$ ./equality_test.sh
testEquality

Ran 1 test.

OK

bats-core

#!/usr/bin/env bats

@test "addition using bc" {
  result="$(echo 2+2 | bc)"
  [ "$result" -eq 4 ]
}

@test "addition using dc" {
  result="$(echo 2 2+p | dc)"
  [ "$result" -eq 4 ]
}

shellspec plan to take a different approach.

#!/bin/bash
# No need special shebang

eval "$(shellspec -)" # Magical words is on the top not the bottom

Describe 'bc command'
  add() { echo "$1 + $2" | bc; }

  It 'performs addition'
    When call add 2 3
    The output should eq 5
  End
End
$ ./test.sh -f tap # Supports all shellspec options

Why it does work?

shellspec - outputs exec /bin/bash shellspec "$0" "$@". Therefore the execution of specfile stopped at the line, and re-execute by shellspec.

test timing information for JUnit XML and profiler

Currently, timing information for JUnit XML is missing. And profiler can be implement if exists timing information.

The easiest way is

start_time=$(date +%s%N)
run example
end_time=$(date +%s%N)
timing=$((end_time - start_time))

But this approache has problems.

  1. Getting milli / nano seconds is neither POSIX nor portable.
  2. Increase execution time (approximately x1.6)

So, I'm thinking of another approach.

This is pseudo code.

[shellspec-profiler.sh]
counter=0
trap 'echo $counter >> profile.txt' USR1
while :; do
  counter=$((counter + 1)
done

[shellspec]
shellspec-profiler.sh &
PROFILER_PID=$!

[specfile execution]
kill -USR1 $PROFILER_PID
run example
kill -USR1 $PROFILER_PID

Two values ​​are written to profile.txt for each example. The entire execution time can be obtained in milliseconds by time command. It means, the execution time of each example can be calculated. (and cpu gets hot. ♨️)

/dev/tty: No such device or address

Getting this error when attempting to run shellspec on my Jenkins server:

/var/lib/jenkins/lib/shellspec/libexec/shellspec-translate.sh: line 198: /dev/tty: No such device or address

I tried to define SHELLSPEC_LOGFILE and also --no-color without success.
My guess is that's unintended?

Edit 1:
After looking into the code I tried echo "foo" > /dev/tty while logged into SSH, and that worked. But putting the same command into the Jenkins build step fails with the same error message.

Edit 2:
After more Google I tried [ -t 1 ] && echo yay || echo nay in SSH and in Jenkins. The true case means TTY is available. Maybe use that?

Use default $HOME/.local install directory instead of $HOME/bin and $HOME/lib

In my Ubuntu .profile there is

**# set PATH so it includes user's private bin if it exists
if [ -d "$HOME/bin" ] ; then
    PATH="$HOME/bin:$PATH"
fi

# set PATH so it includes user's private bin if it exists
if [ -d "$HOME/.local/bin" ] ; then
    PATH="$HOME/.local/bin:$PATH"
fi
**

And in the curl -fsSL https://git.io/shellspec installer, the default installation folders are $HOME/bin and $HOME/lib. And the lib folder is only used by shellspec contrary to my .local/lib which is used by a bunch of programs including python's module:

ls -l ~/.local/bin/
total 748
-rwxrwxr-x 1 jean jean    218 nov.   9 21:52 f2py
-rwxrwxr-x 1 jean jean    218 nov.   9 21:52 f2py2
-rwxrwxr-x 1 jean jean    218 nov.   9 21:52 f2py2.7
-rwxr-xr-x 1 jean jean    741 nov.  10 08:49 fluxgui
-rwxrwxr-x 1 jean jean   3770 nov.   9 22:01 mid3cp
-rwxrwxr-x 1 jean jean   5258 nov.   9 22:01 mid3iconv
-rwxrwxr-x 1 jean jean  14767 nov.   9 22:01 mid3v2
-rwxrwxr-x 1 jean jean   2479 nov.   9 22:01 moggsplit
-rwxrwxr-x 1 jean jean   1239 nov.   9 22:01 mutagen-inspect
-rwxrwxr-x 1 jean jean   3305 nov.   9 22:01 mutagen-pony
lrwxrwxrwx 1 jean jean     49 déc.   2 20:49 php -> /home/jean/.local/lib/docker-php-multiversion/php
-rwxrwxr-x 1 jean jean 706004 sept. 27  2013 xflux

This has one advantage: it keeps your home folder cleaner (Btw I copied this script for my php-multiversion tool and moved it to .local as default).

I just opened this bug as I don't know if your realized that but I think it would be a better place for installing shellspec. If you disagree, simply close it, my opinion is nothing more than my opinion

readlink/dirname issue due to the space in "my_lib [specfiles]"

I had another issue due to the space in the filename: When I used

script_dir=$(dirname $(readlink -f "$0"))
source $script_dir/functions.bash 

The row dirname $(readlink -f "$0") returned /path/to/script . only when run by shellspec which was a false negative, trying to include /path/to/script ./functions.bash.

The workaround I found was to quote the result of readlink script_dir=$(dirname "$(readlink -f "$0")") but removing the space seems more a more robust fix to me

Unexpected error when using setup with bash shell (but ok with sh)

Hello,
A have a test scenario with a setup function:

Describe "Problem SSH during setup"

    setup() {
        ssh -o ConnectTimeout=2 -q [email protected] 'touch /tmp/foo.txt'
    }

    teardown() {
        ssh -q [email protected] 'rm -f /tmp/foo.txt'
    }

    BeforeAll setup
    AfterAll  teardown

    Describe "Ability to check file in temp dir"

        It "File foo.txt exist"
            When run ssh -o ConnectTimeout=2 -q [email protected] '[ -e /tmp/foo.txt ]'
            The status should be success
        End

    End

End

When using this file with sh shell (./shellspec -s /bin/sh test_spec.sh), the restult is OK:

Running: /bin/sh [sh]

Problem SSH during setup
  Ability to check file in temp dir
    File foo.txt exist

Finished in 0.62 seconds (user 0.06 seconds, sys 0.21 seconds)
1 example, 0 failures

But when using this file with bash shell (./shellspec -s /bin/bash test_spec.sh), the restult is OK (the expectation is green), but the execution ended with an unexpected error. I do not understand why...

Running: /bin/bash [bash 4.4.20(1)-release]

Problem SSH during setup
  Ability to check file in temp dir
    File foo.txt exist

Finished in 0.70 seconds (user 0.09 seconds, sys 0.17 seconds)
1 example, 0 failures, aborted by an unexpected error

An unexpected error occurred. [executor: 0] [reporter: 1] [error handler: 0]
Fatal error occurred, terminated with exit status 1.

I am running shellspec version 0.24.3.

Thank you !

`result of function` fails to run subject containing spaces when whitespace is not in IFS

When using Bash strict mode, IFS is set to $'\n\t' while it is set to $' \t\n\0' by default (on my machine).

It causes the result modifier on function subject to fail running a function while passing arguments (e.g. foo --bar).

The following specification illustrates the bug:

#shellcheck shell=bash

fContext "When using Bash strict mode"
    Skip if "not on Bash" test "${BASH:-}"x = x
    IFS=$'\n\t'
    # IFS=$' \t\n\0'

    Context "the function subject"
        foo() {
            echo "Foo output"
        }

        It "runs a function"
            The result of function "foo" should equal "Foo output"
        End

        It "runs a function with arguments"
            The result of function "foo --bar" should equal "Foo output"
        End
    End
End
$ bin/shellspec
0.21.0-dev
$ bin/shellspec --shell bash -f d --focus
Running: /usr/bin/bash [bash 5.0.11(1)-release]

When using Bash strict mode
  the function subject
    runs a function
    runs a function with arguments (FAILED - 1)

Examples:
  1) When using Bash strict mode the function subject runs a function with arguments
     1.1) The result of function foo --bar should equal Foo output

            expected: "Foo output"
                 got: <unset>
            
          # spec/bugs/strict_bugs_spec.sh:18

Finished in ? seconds (user ? seconds, sys ? seconds)
2 examples, 1 failure

Notable examples: (Listed here do not affect your suite's status)

shellspec spec/bugs/strict_bugs_spec.sh:13 # -) When using Bash strict mode the function subject runs a function 

Failure examples: (Listed here affect your suite's status)

shellspec spec/bugs/strict_bugs_spec.sh:17 # 1) When using Bash strict mode the function subject runs a function with arguments FAILED

Unable to exclude "my_lib [specfiles]" from coverage

I'm presently testing bash script which have no .sh extension so the kcov --include-pattern=.sh option must remain empty.
Due to that, the "my lib [specfiles]" temporary file is included into the code coverage target and produces an error when a try to add it to the --exclude-pattern option:

kcov: error: Can't find or open /home/jean/dev/php-library-development-toolkit/coverage

Unexpected error (syntax error?) occurred in ''


Finished in 0.06 seconds (user 0.05 seconds, sys 0.01 seconds)
0 examples, 0 failures, aborted by an unexpected error

An unexpected error occurred or output to the stderr. [0] [1] [102]
Fatal error occurred, terminated with exit status 1.

Wouldn't it be more simple to save this file in the /tmp directory instead of hacking coverage options?

Regards

Question: How to share states between examples in a group?

Given:

hello () {
    pwd
    echo subshell pid is: $BASHPID >&2
    echo ABC=$ABC >&2
} 

change_dir () {
    echo change_dir subshell pid is: $BASHPID >&2
    cd /tmp
    export ABC=123
}

Describe "hello()"
  BeforeAll change_dir

  It "does something 1"
    When call hello
    The output should include /tmp
  End

  It "does something 2"
    When call hello
    The output should include /tmp
  End
End

I would expect the CWD of both examples to be /tmp and env var ABC to be 123 but it turns out that's only true for the first example. See output below:

Running: /bin/bash [bash 4.4.20(1)-release]
change_dir subshell pid is: 32073

Unexpected output to the stderr at line 16-18 in 'spec/sss_spec.sh'
WF

Examples:
  1) hello() does something 1
     call hello

     1.1) [WARNING] It was output to stderr but not found expectation

            stderr: subshell pid is: 32074
            ABC=123
            
          # spec/sss_spec.sh:16

  2) hello() does something 2
     call hello

     2.1) The output should include /tmp

            expected "/home/ubuntu/work/sss" to include "/tmp"
            
          # spec/sss_spec.sh:23

     2.2) [WARNING] It was output to stderr but not found expectation

            stderr: subshell pid is: 32078
            ABC=
            
          # spec/sss_spec.sh:21

Finished in 0.15 seconds (user 0.14 seconds, sys 0.03 seconds)
2 examples, 1 failure, 1 warning


Failure examples: (Listed here affect your suite's status)

shellspec spec/sss_spec.sh:16 # 1) hello() does something 1 WARNED
shellspec spec/sss_spec.sh:21 # 2) hello() does something 2 FAILED

Is this the expected result? If each example group is a subshell then can we make it the parent, or even an ancestor, process of the subshells of each examples under it?

Thanks

Can not get coverage with `When run bash-script`

spec/test_spec.sh

It "runs ./a.sh"
  When run ./a.sh
  The stdout should eq "ok"
End

./a.sh

#!/bin/bash
echo ok
$ shellspec --kcov
Running: /bin/bash [bash 4.4.20(1)-release]
.

Finished in 1.09 seconds (user 0.15 seconds, sys 0.68 seconds)
1 example, 0 failures

Code covered: 0.00%, Executed lines: 0, Instrumented lines: 1

There is one Instrumented lines and executed, but code covered is 0.00%.

--xtrace not respected

Thanks for all the work you have put into ShellSpec, and for making it available.

I'm trying to debug a Unexpected error (syntax error?) occurred in ''.
In the following --xtrace seems to give no additional info.

$ shellspec --no-banner --xtrace spec/kfc-reset-env_spec.sh 
Fall back to trace-only mode. All expectations will be skipped.
+ set +o xtrace

Running: /bin/sh [sh] {--trace-only}
.
[spec/kfc-reset-env_spec.sh:10] run script ./libexec/kfc-reset-env.sh
Unexpected error (syntax error?) occurred in ''
/bin/sh: 1: export: ure/env-script.sh: bad variable name


Finished in 0.10 seconds (user 0.05 seconds, sys 0.01 seconds)
1 example, 0 failures [trace-only mode]

An unexpected error occurred. [executor: 0] [reporter: 0] [error handler: 102]
Fatal error occurred, terminated with exit status 102.

Appreciate any hints or tips.

Passing environment variables inline during "run source" produces not found error

Here is my use case

    fIt "runs php with environment variables"
        When run source COMPOSER_VENDOR_DIR='custom_vendor' ./php spec/getenv_composer_vendor_dir.php 
        The stdout should eq "COMPOSER_VENDOR_DIR=custom_vendor"
        The stderr should eq ""
    End

And I get a not found error because Shellspec tries to run the environment variable COMPOSER_VENDOR_DIR='custom_vendor'.

I gave a look to shellspec_evaluation_run_source and shellspec_interceptor but it's too tricky for me, sorry

As a workaround, I use export to set my environment variable

        COMPOSER_VENDOR_DIR='custom_vendor'
        export COMPOSER_VENDOR_DIR
        When run source ./php spec/getenv_composer_vendor_dir.php
        The stdout should eq "COMPOSER_VENDOR_DIR=custom_vendor"
        The stderr should eq ""

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.