shdotenv

The atago project wrote these specs on its own initiative and runs them in its own CI, to exercise atago against a real program. They are not shdotenv’s official test suite, and the shdotenv project is not affiliated with atago.

Summary #

2 suites · 17 scenarios

Contents #

shdotenv (dialects and output formats) #

The two matrices shdotenv exists for: the .env syntax dialect it reads, and the format it writes.

A dialect is not a preference — it changes what a file means, so the scenarios here feed one file to several dialects and assert that the answers differ in the documented way: a backslash-n that stays two characters under the POSIX rules and becomes a newline under Ruby’s, a key with a space after it that POSIX and Docker refuse and Ruby accepts. The format half is asserted exactly, because these outputs are consumed by other programs: a shell, csh, fish, JSON, JSONL, YAML.

Source: test/e2e/thirdparty/shdotenv/dialects.atago.yaml

Scenario: one file, six formats, each exactly as its consumer expects #

only when command -v shdotenv succeeds · skipped on Windows

Given #

  • Fixture file .env is created.
  • Environment variables are set: LC_ALL.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).
  • Environment variables are set: LC_ALL.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).
  • Environment variables are set: LC_ALL.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).
  • Environment variables are set: LC_ALL.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).
  • Environment variables are set: LC_ALL.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).
  • Environment variables are set: LC_ALL.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).

Inputs #

Fixture .env:

NAME=world
GREETING="hello, ${NAME}"

When #

shdotenv --format sh
shdotenv --format csh
shdotenv --format fish
shdotenv --format json
shdotenv --format jsonl
shdotenv --format yaml

Then #

  • after shdotenv --format sh:
    • exit code is 0
    • stdout equals an exact value
  • after shdotenv --format csh:
    • exit code is 0
    • stdout equals an exact value
  • after shdotenv --format fish:
    • exit code is 0
    • stdout equals an exact value
  • after shdotenv --format json:
    • exit code is 0
    • stdout at $.NAME equals world; at $.GREETING equals hello, world
  • after shdotenv --format jsonl:
    • exit code is 0
    • stdout equals an exact value
  • after shdotenv --format yaml:
    • exit code is 0
    • stdout equals an exact value

Expected output #

expected stdout:

export NAME='world'
export GREETING='hello, world'

expected stdout:

setenv NAME 'world';
setenv GREETING 'hello, world';

expected stdout:

set --export NAME 'world';
set --export GREETING 'hello, world';

expected stdout:

{ "NAME": "world", "GREETING": "hello, world" }

expected stdout:

NAME: "world"
GREETING: "hello, world"

Scenario: the output can be filtered and reduced to names, and sorted only partly #

only when command -v shdotenv succeeds · skipped on Windows

Given #

  • Fixture file .env is created.
  • Environment variables are set: LC_ALL.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).
  • Environment variables are set: LC_ALL.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).
  • Environment variables are set: LC_ALL.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).
  • Environment variables are set: LC_ALL.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).
  • Environment variables are set: LC_ALL.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).

Inputs #

Fixture .env:

DELTA=4
BRAVO=2
ECHO=5
ALFA=1
CHARLIE=3

When #

shdotenv
shdotenv --sort
shdotenv --grep '^[BC]'
shdotenv --name-only
shdotenv --name-only --sort

Then #

  • after shdotenv:
    • exit code is 0
    • stdout equals an exact value
  • after shdotenv --sort:
    • exit code is 0
    • stdout equals an exact value
  • after shdotenv --grep '^[BC]':
    • exit code is 0
    • stdout equals an exact value
  • after shdotenv --name-only:
    • exit code is 0
    • stdout equals an exact value
  • after shdotenv --name-only --sort:
    • exit code is 0
    • stdout equals an exact value

Expected output #

expected stdout:

export DELTA='4'
export BRAVO='2'
export ECHO='5'
export ALFA='1'
export CHARLIE='3'

expected stdout:

export ALFA='1'
export BRAVO='2'
export DELTA='4'
export ECHO='5'
export CHARLIE='3'

expected stdout:

export BRAVO='2'
export CHARLIE='3'

expected stdout:

DELTA
BRAVO
ECHO
ALFA
CHARLIE

expected stdout:

DELTA
BRAVO
ECHO
ALFA
CHARLIE

Scenario: a backslash escape means different things in different dialects #

only when command -v shdotenv succeeds · skipped on Windows

Given #

  • Fixture file .env is created.
  • Environment variables are set: LC_ALL.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).
  • Environment variables are set: LC_ALL.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).
  • Environment variables are set: LC_ALL.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).
  • Environment variables are set: LC_ALL.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).

Inputs #

Fixture .env:

MESSAGE="first\nsecond"

When #

shdotenv --dialect posix --format json
shdotenv --dialect ruby --format json
shdotenv --dialect node --format json
shdotenv --dialect go --format json

Then #

  • after shdotenv --dialect posix --format json:
    • exit code is 0
    • stdout at $.MESSAGE equals first\nsecond
  • after shdotenv --dialect ruby --format json:
    • exit code is 0
    • stdout at $.MESSAGE equals "first\nsecond"
  • after shdotenv --dialect node --format json:
    • exit code is 0
    • stdout at $.MESSAGE equals "first\nsecond"
  • after shdotenv --dialect go --format json:
    • exit code is 0
    • stdout at $.MESSAGE equals "first\nsecond"

Scenario: a space after the key is a syntax error in some dialects and not in others #

only when command -v shdotenv succeeds · skipped on Windows

Given #

  • Fixture file .env is created.
  • Environment variables are set: LC_ALL.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).
  • Environment variables are set: LC_ALL.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).
  • Environment variables are set: LC_ALL.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).

Inputs #

Fixture .env:

KEY = value

When #

shdotenv --dialect posix
shdotenv --dialect docker
shdotenv --dialect ruby

Then #

  • after shdotenv --dialect posix:
    • exit code is 1
    • stdout is empty
    • stderr equals an exact value
  • after shdotenv --dialect docker:
    • exit code is 1
    • stderr equals an exact value
  • after shdotenv --dialect ruby:
    • exit code is 0
    • stdout equals an exact value

Expected output #

expected stderr:

shdotenv: `KEY ': no space allowed after the key

expected stderr:

shdotenv: `KEY ': no space allowed after the key

expected stdout:

export KEY='value'

Scenario: a line that is not a definition is quoted back with what is wrong #

only when command -v shdotenv succeeds · skipped on Windows

Given #

  • Fixture file .env is created.
  • Environment variables are set: LC_ALL.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).
  • Fixture file .env is created.
  • Environment variables are set: LC_ALL.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).

Inputs #

Fixture .env:

GOOD=value
THIS IS NOT A DEFINITION

Fixture .env:

A-B=value

When #

shdotenv
shdotenv

Then #

  • after shdotenv:
    • exit code is 1
    • stdout is empty
    • stderr equals an exact value
  • after shdotenv:
    • exit code is 1
    • stderr equals an exact value

Expected output #

expected stderr:

shdotenv: `THIS IS NOT A DEFINITION': not a variable definition

expected stderr:

shdotenv: `A-B': the key is not a valid identifier

Scenario: the environment can be dumped as a .env file and read back #

only when command -v shdotenv succeeds · skipped on Windows

Given #

  • Environment variables are set: LC_ALL, OTHER_VALUE, ROUND_TRIP.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).
  • Environment variables are set: LC_ALL.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).
  • Environment variables are set: LC_ALL, ROUND_TRIP.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).

When #

shdotenv export ROUND_TRIP OTHER_VALUE
shdotenv -e dumped.env --format json
shdotenv export -n ROUND_TRIP

Then #

  • after shdotenv export ROUND_TRIP OTHER_VALUE:
    • exit code is 0
    • the step changed exactly created dumped.env, modified nothing, deleted nothing
  • after shdotenv -e dumped.env --format json:
    • exit code is 0
    • stdout at $.ROUND_TRIP equals a value with spaces; at $.OTHER_VALUE equals x'y
  • after shdotenv export -n ROUND_TRIP:
    • exit code is 0
    • stdout equals an exact value

Expected output #

expected stdout:

ROUND_TRIP

Generated artifacts #

  • dumped.env

Scenario: a file can come from standard input #

only when command -v shdotenv succeeds · skipped on Windows

Given #

  • Environment variables are set: LC_ALL.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).

Inputs #

stdin for shdotenv:

FROM_STDIN=yes
SECOND=2

When #

shdotenv -e -

Then #

  • exit code is 0
  • stdout equals an exact value
  • the step changed exactly created nothing, modified nothing, deleted nothing

Expected output #

expected stdout:

export FROM_STDIN='yes'
export SECOND='2'

shdotenv (parsing a .env file safely) #

shdotenv loads .env files into the environment without letting the file run anything. Its own test suite is written in ShellSpec; what those tests check is pinned here from outside.

The safety promise is the center of this file, and it is asserted the only way a security claim should be: a .env file that tries to run a command is given every chance to, and the proof is that the file it would have created does not exist. Around it are the parsing rules, the strict defaults that make the tool usable in CI (an unset reference is an error, two files defining the same key is an error), and the precedence between the file and the environment already in place.

Source: test/e2e/thirdparty/shdotenv/shdotenv.atago.yaml

Scenario: the version, the help, and an option that does not exist #

only when command -v shdotenv succeeds · skipped on Windows

Given #

  • Environment variables are set: LC_ALL.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).
  • Environment variables are set: LC_ALL.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).
  • Environment variables are set: LC_ALL.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).

When #

shdotenv --version
shdotenv --help
shdotenv --nope

Then #

  • after shdotenv --version:
    • exit code is 0
    • stdout matches /^[0-9]+\.[0-9]+\.[0-9]+\n$/
  • after shdotenv --help:
    • exit code is 0
    • stdout contains Usage: shdotenv [OPTION]... [--] [[COMMAND | export] [ARG]...], -d, --dialect DIALECT, -f, --format FORMAT
  • after shdotenv --nope:
    • exit code is 1
    • stdout is empty
    • stderr contains --nope

Scenario: a file is read as data and printed as shell that can be evaluated #

only when command -v shdotenv succeeds · skipped on Windows

Given #

  • Fixture file .env is created.
  • Environment variables are set: LC_ALL.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).
  • Environment variables are set: LC_ALL.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).

Inputs #

Fixture .env:

# a comment line
NAME=world
GREETING="hello, ${NAME}"
EMPTY=
QUOTED='no ${expansion} here'

When #

shdotenv
shdotenv -- sh -c "echo [$GREETING]"

Then #

  • after shdotenv:
    • exit code is 0
    • stdout equals an exact value
  • after shdotenv -- sh -c "echo [$GREETING]":
    • exit code is 0
    • stdout equals an exact value

Expected output #

expected stdout:

export NAME='world'
export GREETING='hello, world'
export EMPTY=''
export QUOTED='no ${expansion} here'

expected stdout:

[hello, world]

Scenario: a file that tries to run a command runs nothing #

only when command -v shdotenv succeeds · skipped on Windows

Given #

  • Fixture file .env is created.
  • Environment variables are set: LC_ALL.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).
  • Fixture file .env is created.
  • Environment variables are set: LC_ALL.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).
  • Fixture file .env is created.
  • Environment variables are set: LC_ALL.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).

Inputs #

Fixture .env:

EVIL=$(touch pwned-unquoted)

Fixture .env:

EVIL="$(touch pwned-quoted)"

Fixture .env:

EVIL="`touch pwned-backtick`"

When #

shdotenv
shdotenv
shdotenv

Then #

  • after shdotenv:
    • exit code is 1
    • stdout is empty
    • stderr contains spaces are not allowed without quoting
  • after shdotenv:
    • exit code is 1
    • stdout is empty
    • stderr contains the following metacharacters must be escaped: $`"\
  • after shdotenv:
    • exit code is 1
    • stderr contains the following metacharacters must be escaped: $`"\
    • the step changed exactly created nothing, modified nothing, deleted nothing
    • file pwned-unquoted does not exist
    • file pwned-quoted does not exist
    • file pwned-backtick does not exist

Scenario: shell metacharacters inside a value stay text, even when a command runs #

only when command -v shdotenv succeeds · skipped on Windows

Given #

  • Fixture file .env is created.
  • Environment variables are set: LC_ALL.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).
  • Environment variables are set: LC_ALL.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).

Inputs #

Fixture .env:

PAYLOAD="x; touch pwned-semicolon"

When #

shdotenv
shdotenv -- sh -c "echo [$PAYLOAD]"

Then #

  • after shdotenv:
    • exit code is 0
    • stdout equals an exact value
  • after shdotenv -- sh -c "echo [$PAYLOAD]":
    • exit code is 0
    • stdout equals an exact value
    • file pwned-semicolon does not exist

Expected output #

expected stdout:

export PAYLOAD='x; touch pwned-semicolon'

expected stdout:

[x; touch pwned-semicolon]

Scenario: a reference to something unset is an error unless it is allowed #

only when command -v shdotenv succeeds · skipped on Windows

Given #

  • Fixture file .env is created.
  • Environment variables are set: LC_ALL.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).
  • Environment variables are set: LC_ALL.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).

Inputs #

Fixture .env:

VALUE="${NOT_SET_ANYWHERE}"

When #

shdotenv
shdotenv --no-nounset

Then #

  • after shdotenv:
    • exit code is 1
    • stdout is empty
    • stderr equals an exact value
  • after shdotenv --no-nounset:
    • exit code is 0
    • stdout equals an exact value

Expected output #

expected stderr:

shdotenv: NOT_SET_ANYWHERE: the key is not set

expected stdout:

export VALUE=''

Scenario: the environment already in place is visible, and can be hidden #

only when command -v shdotenv succeeds · skipped on Windows

Given #

  • Fixture file .env is created.
  • Environment variables are set: LC_ALL, OUTSIDE.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).
  • Environment variables are set: LC_ALL, OUTSIDE.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).

Inputs #

Fixture .env:

DERIVED="from ${OUTSIDE}"

When #

shdotenv
shdotenv --ignore-environment

Then #

  • after shdotenv:
    • exit code is 0
    • stdout equals an exact value
  • after shdotenv --ignore-environment:
    • exit code is 1
    • stderr equals an exact value

Expected output #

expected stdout:

export DERIVED='from the-host'

expected stderr:

shdotenv: OUTSIDE: the key is not set

Scenario: the file is a default until overload is asked for #

only when command -v shdotenv succeeds · skipped on Windows

Given #

  • Fixture file .env is created.
  • Environment variables are set: LC_ALL, PRESET.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).
  • Environment variables are set: LC_ALL, PRESET.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).
  • Environment variables are set: LC_ALL, PRESET.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).
  • Environment variables are set: LC_ALL, PRESET.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).

Inputs #

Fixture .env:

PRESET=from-the-file

When #

shdotenv -- sh -c "echo [$PRESET]"
shdotenv --overload -- sh -c "echo [$PRESET]"
shdotenv
shdotenv --overload

Then #

  • after shdotenv -- sh -c "echo [$PRESET]":
    • exit code is 0
    • stdout equals an exact value
  • after shdotenv --overload -- sh -c "echo [$PRESET]":
    • exit code is 0
    • stdout equals an exact value
  • after shdotenv:
    • exit code is 0
    • stdout is empty
  • after shdotenv --overload:
    • exit code is 0
    • stdout equals an exact value

Expected output #

expected stdout:

[from-the-environment]

expected stdout:

[from-the-file]

expected stdout:

export PRESET='from-the-file'

Scenario: two files that define the same key are refused rather than merged #

only when command -v shdotenv succeeds · skipped on Windows

Given #

  • Fixture file first.env is created.
  • Fixture file second.env is created.
  • Environment variables are set: LC_ALL.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).
  • Fixture file second.env is created.
  • Environment variables are set: LC_ALL.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).

Inputs #

Fixture first.env:

SHARED=one
ONLY_FIRST=1

Fixture second.env:

SHARED=two
ONLY_SECOND=2

Fixture second.env:

ONLY_SECOND=2

When #

shdotenv -e first.env -e second.env
shdotenv -e first.env -e second.env

Then #

  • after shdotenv -e first.env -e second.env:
    • exit code is 1
    • stdout is empty
    • stderr equals an exact value
  • after shdotenv -e first.env -e second.env:
    • exit code is 0
    • stdout equals an exact value

Expected output #

expected stderr:

shdotenv: second.env: `SHARED' is already defined in the first.env

expected stdout:

export SHARED='one'
export ONLY_FIRST='1'
export ONLY_SECOND='2'

Scenario: a file that is not there is not an error #

only when command -v shdotenv succeeds · skipped on Windows

Given #

  • Environment variables are set: LC_ALL.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).

When #

shdotenv -e missing.env

Then #

  • exit code is 0
  • stdout is empty
  • stderr is empty

Scenario: quiet mode validates a file without printing it #

only when command -v shdotenv succeeds · skipped on Windows

Given #

  • Fixture file .env is created.
  • Environment variables are set: LC_ALL.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).
  • Fixture file .env is created.
  • Environment variables are set: LC_ALL.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).

Inputs #

Fixture .env:

GOOD=value

Fixture .env:

THIS IS NOT A DEFINITION

When #

shdotenv --quiet
shdotenv --quiet

Then #

  • after shdotenv --quiet:
    • exit code is 0
    • stdout is empty
  • after shdotenv --quiet:
    • exit code is 1
    • stdout is empty
    • stderr equals an exact value

Expected output #

expected stderr:

shdotenv: `THIS IS NOT A DEFINITION': not a variable definition