Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make it POSIX #56

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 122 additions & 71 deletions bin/shpec
Original file line number Diff line number Diff line change
@@ -1,41 +1,51 @@
#!/usr/bin/env bash
VERSION=0.1.2
examples=0
failures=0
test_indent=0
red="\033[0;31m"
green="\033[0;32m"
norm="\033[0m"
#
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this line have a purpose?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A shell script traditionally started with just ': '. Before kernels interpreted the hasbang,
the shell would see if the first character(s) equalled ':' or ': '. In that case it would fork itself and feed itself the script.

I don't know how I ended up adding an empty comment up there.

: Use one of: bash, dash, or ksh93
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is simply a comment, correct? Is there a reason it begins with a : and not a #?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hlangeveld A specific reason you left off zshor you just did not precise it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know what I need to do to zsh to make it accept pure POSIX shell.
It's a powerful shell for interactive use, and has some nifty features, but I gave up on zsh long ago for scripting.
Sorry.


indent() {
printf '%*s' $(((test_indent - 1) * 2))
printf '%*s' $(( (test_indent - 1) * 2))
}

# 'echo -e' is not a POSIX feature.
# This helps us to work around that.
echo_needs_minus_e(){
echo "\010"| (read a; [ ${#a} -ne 1 ])
}

if echo_needs_minus_e; then
echoe() { echo -e "$@"; }
else
echoe() { echo "$@"; }
fi

iecho() {
indent && echo "$@"
indent && echoe "$@"
}

sanitize() {
IFS= echo -e "$1" | tr '\n' 'n' | tr "'" 'q'
IFS= echoe "$1" | tr '\n' 'n' | tr "'" 'q'
}

describe() {
((test_indent += 1))
: $((test_indent += 1))
iecho "$1"
}

end() {
((test_indent -= 1))
: $((test_indent -=1 ))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whitespace here is inconsistent with the near-identical line 29 above.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Will fix.

if [ $test_indent -eq 0 ]; then
[ $failures -eq 0 ]
fi
}

end_describe() {
iecho "Warning: end_describe will be deprecated in shpec 1.0. Please use end instead."
iecho "Warning: end_describe will be deprecated in shpec 1.0." \
"Please use end instead."
end
}

# Beware: POSIX shells are not required to accept
# any identifier as a function name.

stub_command() {
body="${2:-:}"
eval "$1() { $body; }"
Expand All @@ -44,97 +54,138 @@ stub_command() {
unstub_command() { unset -f "$1"; }

it() {
((test_indent += 1))
((examples += 1))
: $((test_indent += 1))
: $((examples += 1))
assertion="$1"
}

assert() {
case "x$1" in

xequal )
print_result "[[ '$(sanitize "$2")' = '$(sanitize "$3")' ]]" "Expected [$2] to equal [$3]";;

print_result "[ '$(sanitize "$2")' = '$(sanitize "$3")' ]" \
"Expected [$2] to equal [$3]"
;;
xunequal )
print_result "[[ '$(sanitize "$2")' != '$(sanitize "$3")' ]]" "Expected [$2] not to equal [$3]";;

print_result "[ '$(sanitize "$2")' != '$(sanitize "$3")' ]" \
"Expected [$2] not to equal [$3]"
;;
xgt )
print_result "[[ $2 -gt $3 ]]" "Expected [$2] to be > [$3]";;

print_result "[ $2 -gt $3 ]" \
"Expected [$2] to be > [$3]"
;;
xlt )
print_result "[[ $2 -lt $3 ]]" "Expected [$2] to be < [$3]";;

print_result "[ $2 -lt $3 ]" \
"Expected [$2] to be < [$3]"
;;
xmatch )
print_result "[[ '$2' =~ $3 ]]" "Expected [$2] to match [$3]";;

print_result "case '$2' in *$3*) :;; *) false;; esac" \
"Expected [$2] to match [$3]"
;;
xno_match )
print_result "[[ ! '$2' =~ $3 ]]" "Expected [$2] not to match [$3]";;

print_result "case '$2' in *$3*) false ;; *) :;; esac" \
"Expected [$2] not to match [$3]"
;;
xpresent )
print_result "[[ -n '$2' ]]" "Expected [$2] to be present";;

print_result "[ -n '$2' ]" \
"Expected [$2] to be present"
;;
xblank )
print_result "[[ -z '$2' ]]" "Expected [$2] to be blank";;
print_result "[ -z '$2' ]" \
"Expected [$2] to be blank"
;;

xfile_present )
print_result "[[ -e $2 ]]" "Expected file [$2] to exist";;

print_result "[ -e $2 ]" \
"Expected file [$2] to exist"
;;
xfile_absent )
print_result "[[ ! -e $2 ]]" "Expected file [$2] not to exist";;

print_result "[ ! -e $2 ]" \
"Expected file [$2] not to exist"
;;
xsymlink )
link="$(readlink $2)"
print_result "[[ '$link' = '$3' ]]" "Expected [$2] to link to [$3], but got [$link]";;

print_result "[ '$link' = '$3' ]" \
"Expected [$2] to link to [$3], but got [$link]"
;;
xtest )
print_result "$2" "Expected $2 to be true";;

print_result "$2" \
"Expected $2 to be true"
;;
* )
if type "$1" 2>/dev/null | grep -q 'function'; then
matcher="$1"; shift
$matcher "$@"
else
print_result false "Error: Unknown matcher [$1]"
fi;;

fi
;;
esac
}

print_result() {
if eval "$1"; then
iecho -e "$green$assertion$norm"
iecho "$green$assertion$norm"
else
((failures += 1))
iecho -e "$red$assertion"
iecho -e "($2)$norm"
: $((failures += 1))
iecho "$red$assertion"
iecho "($2)$norm"
fi
}

SHPEC_ROOT=${SHPEC_ROOT:-$([[ -d './shpec' ]] && echo './shpec' || echo '.')}

case "$1" in

-v|--version )

echo "$VERSION";;

* )

matcher_files="$(find "$SHPEC_ROOT/matchers" -name '*.sh' 2>/dev/null)"

for matcher_file in $matcher_files; do
. "$matcher_file"
done

files="${@:-$(find $SHPEC_ROOT -name '*_shpec.sh')}"

time for file in $files; do
. "$file"
done

[ $failures -eq 0 ] && color=$green || color=$red
echo -e "${color}${examples} examples, ${failures} failures${norm}"
shpec() {
(
VERSION=0.1.2posix
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep the version numbers compliant with semver, i.e. just MAJOR.MINOR.PATCH. I think a minor version bump is appropriate here.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I concur, the introduction of the function deserve a 0.2:)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do.

examples=0
failures=0
test_indent=0
red="\033[0;31m"
green="\033[0;32m"
norm="\033[0m"

SHPEC_ROOT=${SHPEC_ROOT:-$([ -d './shpec' ] && echo './shpec' || echo '.')}
case "$1" in
( -v | --version ) echo "$VERSION"
;;
( * )
matcher_files="$(
find "$SHPEC_ROOT/matchers" -name '*.sh' 2>/dev/null
)"

for matcher_file in $matcher_files; do
. "$matcher_file"
done

if [ $# -gt 0 ] ; then
: \$1="'$1'" \${#1}="'${#1}'"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain this line?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which line do you refer? (since we get a ten line diff?)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm referring to line 160. Github will always put the comment directly below the line in question.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line 160, I presume. That is a left over from a 'live comment'.

: almost reads like a comment, but it's more than that. It's actually a proper
command that accepts arguments, and discards them.

Therefore it's useful for showing the value of parameters during execution, and will
show up in '$SHELL -x script'.

It served its purpose, and should go.

files="${@}"
else
files=$(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please quote this in the same manner as matcher_files above.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two should be the same. I think that either way should work. I've become more confident with $( ... ) constructs, and believe that no quoting is needed in either case.

find $SHPEC_ROOT -name '*_shpec.sh'
)
fi

for file in $files; do
echo >& 2 "$file"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove this line.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed

. "$file" || exit
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain the intent behind the || exit?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On 04/13/15 04:07, Ryland Herrick wrote:

  •  for file in $files; do
    
  •    echo >& 2 "$file"
    
  •    . "$file" || exit
    

Can you explain the intent behind the ||| exit|?

Hmm... The idea was catching errors in sourcing $file.
The kind of errors where it doesn't make sense to
continue - serious syntax trouble, etc.

However, this would also consider any non-zero $failure count
an error, and stop processing.

I will think about alternatives here, but for now it just
has to go.

Cheers,
HL

done

[ $failures -eq 0 ] && color=$green || color=$red
echoe "${color}${examples} examples, ${failures} failures${norm}"

times
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't even know about times, thanks!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On 04/13/15 04:07, Ryland Herrick wrote:

  •  times
    

Didn't even know about |times|, thanks!

It's the only thing garantueed to be around.

Of course, there's the time keyword, but it's not a builtin
in every POSIX shell, so won't work with functions.

Even so, times is a work-around. Its output is not consistent
across different shells, but at least it's there.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

[ $failures -eq 0 ]
exit
;;
esac
)
}

[ $failures -eq 0 ]
(
_progname=shpec
_pathname=$( command -v $0 )
_cmdname=${_pathname##*/}
_main=shpec

esac
case $_progname in (${_cmdname%.sh}) $_main "$@";; esac
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you mind explaining exactly how this works?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we run the 'shpec' script, we want to make sure that the '$0' basename matches the name we've chosen. ( 'shpec' )

It's a variation on idiom that I have used quite a lot. There is a long standing
tradition to distinguish shell 'source-files' from installed scripts by appending '.sh' to the file in the source code repository. (Going back to sccs and make in system V.)

So we need to get rid of the directory path, and we then need to match against
either "shpec" or "shpec.sh".

So, instead of the external basename, I use the parameter expansion modifiers:

First, command -v $0 will return a fully-qualified path, if there is any.
Then, ${var##*/} will strip the longest left-matching string ending in "/" from $var.
Finally, ${var%.sh} will strip the string ".sh" from the right of $var, if it's there.

Short version, if the name of the script matches 'shpec' or 'shpec.sh', we call the 'shpec' function.

)
30 changes: 19 additions & 11 deletions shpec/shpec_shpec.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ describe "shpec"

describe "equality matcher"
it "handles newlines properly"
string_with_newline_char="new\nline"
string_with_newline_char="new
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this intentionally changed? This test was added specifically for #23.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On 04/13/15 04:08, Ryland Herrick wrote:

In shpec/shpec_shpec.sh #56 (comment):

@@ -35,7 +35,8 @@ describe "shpec"

describe "equality matcher"
it "handles newlines properly"

  •  string_with_newline_char="new\nline"
    
  •  string_with_newline_char="new
    

Was this intentionally changed? This test was added specifically for #23 #23.

I've added a comment to the issue. POSIX just sees two characters there, not a newline.

Bash and ksh support 'ansi-c' strings with the $'...' string notation.

I would recommend to use initialised string constants using printf.

 CHAR_NL=$( printf "\n" )

If we do this once at the top of shpec(), we could rewrite the assertion above as:

 string_with_newline_char="new${CHAR_NL}line"

etc., etc.

Cheers,
Henk

line"
multiline_string='new
line'
assert equal "$multiline_string" "$string_with_newline_char"
Expand Down Expand Up @@ -64,16 +65,25 @@ line'

describe "passing through to the test builtin"
it "asserts an arbitrary algebraic test"
assert test "[[ 5 -lt 10 ]]"
assert test "[ 5 -lt 10 ]"
end
end

describe "stubbing commands"
# only bash lets us redefine 'exit', so use 'false'
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ce61f72 contains the reasoning behind this test. My preference is to leave that kind of thing in git, and not in the code itself.

Additionally, I would say that this initial test is unnecessary; the following two tests cover both the stubbed functionality and the original functionality. If any part of that stub/unstub process fails, one of these commands will indicate so.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On 04/13/15 04:16, Ryland Herrick wrote:

In shpec/shpec_shpec.sh #56 (comment):

 end

end

describe "stubbing commands"

  • only bash lets us redefine 'exit', so use 'false'

ce61f72 ce61f72 contains the reasoning behind this test. My
preference is to leave that kind of thing in git, and not in the code itself.

Agreed.

Additionally, I would say that this initial test is unnecessary; the following two tests cover both the stubbed functionality and
the original functionality. If any part of that stub/unstub process fails, one of these commands will indicate so.
Correct. The first assertion is superfluous.

it "check original working of the stub"
false
assert equal "$?" 1
end
it "stubs to the null command by default"
stub_command "exit"
exit # doesn't really exit
stub_command "false"
false # doesn't do anything
assert equal "$?" 0
unstub_command "exit"
unstub_command "false"
end
it "preserves the original working of the stub"
false
assert equal "$?" 1
end

it "accepts an optional function body"
Expand Down Expand Up @@ -108,14 +118,13 @@ line'
end

describe "exit codes"
shpec_cmd="$SHPEC_ROOT/../bin/shpec"
it "returns nonzero if any test fails"
$shpec_cmd $SHPEC_ROOT/etc/failing_example &> /dev/null
shpec $SHPEC_ROOT/etc/failing_example > /dev/null 2>& 1
assert unequal "$?" "0"
end

it "returns zero if a suite passes"
$shpec_cmd $SHPEC_ROOT/etc/passing_example &> /dev/null
shpec $SHPEC_ROOT/etc/passing_example > /dev/null 2>& 1
assert equal "$?" "0"
end
end
Expand All @@ -133,18 +142,17 @@ line'
end

describe "commandline options"
shpec_cmd="$SHPEC_ROOT/../bin/shpec"

describe "--version"
it "outputs the current version number"
message="$($shpec_cmd --version)"
message="$(shpec --version)"
assert match "$message" "$(cat $SHPEC_ROOT/../VERSION)"
end
end

describe "-v"
it "outputs the current version number"
message="$($shpec_cmd -v)"
message="$(shpec -v)"
assert match "$message" "$(cat $SHPEC_ROOT/../VERSION)"
end
end
Expand Down