I am very new to writing scripts and I am having trouble figuring out how to get started on a bash script that will automatically test the output of a program against expected output.
I want to write a bash script that will run a specified executable on a set of test inputs, say in1 in2 etc., against corresponding expected outputs, out1, out2, etc., and check that they match. The file to be tested reads its input from stdin and writes its output to stdout. So executing the test program on an input file will involve I/O redirection.
The script will be invoked with a single argument, which will be the name of the executable file to be tested.
I'm having trouble just getting going on this, so any help at all (links to any resources that further explain how I could do this) would be greatly appreciated. I've obviously tried searching myself but haven't been very successful in that.
Thanks!
If I get what you want; this might get you started:
A mix of bash + external tools like diff.
#!/bin/bash
# If number of arguments less then 1; print usage and exit
if [ $# -lt 1 ]; then
printf "Usage: %s <application>\n" "$0" >&2
exit 1
fi
bin="$1" # The application (from command arg)
diff="diff -iad" # Diff command, or what ever
# An array, do not have to declare it, but is supposedly faster
declare -a file_base=("file1" "file2" "file3")
# Loop the array
for file in "${file_base[#]}"; do
# Padd file_base with suffixes
file_in="$file.in" # The in file
file_out_val="$file.out" # The out file to check against
file_out_tst="$file.out.tst" # The outfile from test application
# Validate infile exists (do the same for out validate file)
if [ ! -f "$file_in" ]; then
printf "In file %s is missing\n" "$file_in"
continue;
fi
if [ ! -f "$file_out_val" ]; then
printf "Validation file %s is missing\n" "$file_out_val"
continue;
fi
printf "Testing against %s\n" "$file_in"
# Run application, redirect in file to app, and output to out file
"./$bin" < "$file_in" > "$file_out_tst"
# Execute diff
$diff "$file_out_tst" "$file_out_val"
# Check exit code from previous command (ie diff)
# We need to add this to a variable else we can't print it
# as it will be changed by the if [
# Iff not 0 then the files differ (at least with diff)
e_code=$?
if [ $e_code != 0 ]; then
printf "TEST FAIL : %d\n" "$e_code"
else
printf "TEST OK!\n"
fi
# Pause by prompt
read -p "Enter a to abort, anything else to continue: " input_data
# Iff input is "a" then abort
[ "$input_data" == "a" ] && break
done
# Clean exit with status 0
exit 0
Edit.
Added exit code check; And a short walk trough:
This will in short do:
Check if argument is given (bin/application)
Use an array of "base names", loop this and generate real filenames.
I.e.: Having array ("file1" "file2") you get
In file: file1.in
Out file to validate against: file1.out
Out file: file1.out.tst
In file: file2.in
...
Execute application and redirect in file to stdin for application by <, and redirect stdout from application to out file test by >.
Use a tool like i.e. diff to test if they are the same.
Check exit / return code from tool and print message (FAIL/OK)
Prompt for continuance.
Any and all of which off course can be modified, removed etc.
Some links:
TLDP; Advanced Bash-Scripting Guide (can be a bit more readable with this)
Arrays
File test operators
Loops and branches
Exit-status
...
bash-array-tutorial
TLDP; Bash-Beginners-Guide
Expect could be a perfect fit for this kind of problem:
Expect is a tool primarily for automating interactive applications
such as telnet, ftp, passwd, fsck, rlogin, tip, etc. Expect really
makes this stuff trivial. Expect is also useful for testing these same
applications.
First take a look at the Advanced Bash-Scripting Guide chapter on I/O redirection.
Then I have to ask Why use a bash script at all? Do it directly from your makefile.
For instance I have a generic makefile containing something like:
# type 'make test' to run a test.
# for example this runs your program with jackjill.txt as input
# and redirects the stdout to the file jackjill.out
test: $(program_NAME)
./$(program_NAME) < jackjill.txt > jackjill.out
./diff -q jackjill.out jackjill.expected
You can add as many tests as you want like this. You just diff the output file each time against a file containing your expected output.
Of course this is only relevant if you're actually using a makefile for building your program. :-)
Functions. Herestrings. Redirection. Process substitution. diff -q. test.
Expected outputs are a second kind of input.
For example, if you want to test a square function, you would have input like (0, 1, 2, -1, -2) and expected output as (0, 1, 4, 1, 4).
Then you would compare every result of input to the expected output and report errors for example.
You could work with arrays:
in=(0 1 2 -1 -2)
out=(0 1 4 2 4)
for i in $(seq 0 $((${#in[#]}-1)) )
do
(( ${in[i]} * ${in[i]} - ${out[i]} )) && echo -n bad" " || echo -n fine" "
echo $i ": " ${in[i]}"² ?= " ${out[i]}
done
fine 0 : 0² ?= 0
fine 1 : 1² ?= 1
fine 2 : 2² ?= 4
bad 3 : -1² ?= 2
fine 4 : -2² ?= 4
Of course you can read both arrays from a file.
Testing with (( ... )) can invoke arithmetic expressions, strings and files. Try
help test
for an overview.
Reading strings wordwise from a file:
for n in $(< f1); do echo $n "-" ; done
Read into an array:
arr=($(< file1))
Read file linewise:
for i in $(seq 1 $(cat file1 | wc -l ))
do
line=$(sed -n ${i}p file1)
echo $line"#"
done
Testing against program output sounds like string comparison and capturing of program output n=$(cmd param1 param2):
asux:~/prompt > echo -e "foo\nbar\nbaz"
foo
bar
baz
asux:~/prompt > echo -e "foo\nbar\nbaz" > file
asux:~/prompt > for i in $(seq 1 3); do line=$(sed -n ${i}p file); test "$line" = "bar" && echo match || echo fail ; done
fail
match
fail
Further usesful: Regular expression matching on Strings with =~ in [[ ... ]] brackets:
for i in $(seq 1 3)
do
line=$(sed -n ${i}p file)
echo -n $line
if [[ "$line" =~ ba. ]]; then
echo " "match
else echo " "fail
fi
done
foo fail
bar match
baz match
Related
I have a small bash script with a function containing a case statement which echoes random data if the 1st argument matches the case parameter.
Code is as follows:
#!/usr/bin/env bash '
AC='auto-increment'
UUID='uuid'
LAT='lat'
LONG='long'
IP='ip'
generate_mock_data() {
# ARGS: $1 - data type, $2 - loop index
case ${1} in
${AC})
echo ${2} ;;
${UUID})
uuidgen ;;
${LAT})
echo $((RANDOM % 180 - 90)).$(shuf -i1000000-9999999 -n1) ;;
${LONG})
echo $((RANDOM % 360 - 180)).$(shuf -i1000000-9999999 -n1) ;;
${IP})
echo $((RANDOM%256)).$((RANDOM%256)).$((RANDOM%256)).$((RANDOM%256)) ;;
esac
}
# Writing data to file
headers=('auto-increment' 'uuid' 'lat' 'long' 'ip')
for i in {1..2500}; do
for header in "${headers[#]}"; do
echo -n $(generate_mock_data ${header} ${i}),
done
echo # New line
done >> file.csv
However, execution time is incredibly slow for just 2500 rows:
real 0m8.876s
user 0m0.576s
sys 0m0.868s
What am I doing wrong ? is there anything I can do to speed up the process ? or is bash not the right language for these type of operations ?
I also tried profiling the entire script but after looking at the logs I didn't notice any significant bottlenecks.
It seems you can generate a UUID pretty fast with Python, so if you just execute Python once to generate 2,500 UUIDs, and you aren't a Python programmer -like me ;-) then you can patch them up with awk:
python -c 'import uuid; print("\n".join([str(uuid.uuid4()).upper() for x in range(2500)]))' |
awk '{
lat=-90+180*rand();
lon=-180+360*rand();
ip=int(256*rand()) "." int(256*rand()) "." int(256*rand()) "." int(256*rand());
print NR,$0,lat,lon,ip
}' OFS=,
This takes 0.06s on my iMac.
OFS is the "Output Field Separator"
NR is the line number
$0 means "the whole input line"
You can try the Python on its own, like this:
python -c 'import uuid; print("\n".join([str(uuid.uuid4()).upper() for x in range(2500)]))'
Is Shell The Right Tool?
Not really, but if you avoid bad practices, you can make something relatively fast.
With ksh93, the below reliably runs in 0.5-0.6s wall-clock; with bash, 1.2-1.3s.
What Does It Look Like?
#!/usr/bin/env bash
# Comment these two lines if running with ksh93, obviously. :)
[ -z "$BASH_VERSION" ] && { echo "This requires bash 4.1 or newer" >&2; exit 1; }
[[ $BASH_VERSION = [123].* ]] && { echo "This requires bash 4.1 or newer" >&2; exit 1; }
uuid_stream() {
python -c '
import uuid
try:
while True:
print str(uuid.uuid4()).upper()
except IOError:
pass # probably an EPIPE because we were closed.
'
}
# generate a file descriptor that emits a shuffled stream of integers
exec {large_int_fd}< <(while shuf -r -i1000000-9999999; do :; done)
# generate a file descriptor that emits an endless stream of UUIDs
exec {uuid_fd}< <(uuid_stream)
generate_mock_data() {
typeset val
case $1 in
auto-increment) val="$2" ;;
uuid) IFS= read -r val <&"$uuid_fd" || exit;;
lat) IFS= read -r val <&"$large_int_fd" || exit
val="$((RANDOM % 180 - 90)).$val" ;;
long) IFS= read -r val <&"$large_int_fd" || exit
val="$((RANDOM % 360 - 180)).$val" ;;
ip) val="$((RANDOM%256)).$((RANDOM%256)).$((RANDOM%256)).$((RANDOM%256))" ;;
esac
printf '%s' "$val"
}
for ((i=0; i<2500; i++)); do
for header in auto-increment uuid lat long ip; do
generate_mock_data "$header" "$i"
printf ,
done
echo
done > file.csv
What's Different?
There are no command substitutions inside the inner loop. That means we don't ever use $() or any synonym for same. Each of these involves a fork() -- creating a new OS-level copy of the process -- and a wait(), with a bunch of FIFO magic to capture our output.
There are no external commands inside the inner loop. Any external command is even worse than a command substitution: They require a fork, and then additionally require an execve, with the dynamic linker and loader being invoked to pull in all the library dependencies for whichever external command is being run.
Because we don't have a command substitution stripping newlines, we have the function just not emitting them.
I have a list of files:
file_name_FOO31101.txt
file_name_FOO31102.txt
file_name_FOO31103.txt
file_name_FOO31104.txt
And I want to use pairs of files for input into a downstream program such as:
program_call file_name_01.txt file_name_02.txt
program_call file_name_03.txt file_name_04.txt
...
I do not want:
program_call file_name_02.txt file_name_03.txt
I need to do this in a loop as follows:
#!/bin/bash
FILES=path/to/files
for file in $FILES/*.txt;
do
stem=$( basename "${file}" ) # stem : file_name_FOO31104_info.txt
output_base=$( echo $stem | cut -d'_' -f 1,2,3 ) # output_base : FOO31104_info.txt
id=$( echo $stem | cut -d'_' -f 3 ) # get the first field : FOO31104
number=$( echo -n $id | tail -c 2 ) # get the last two digits : 04
echo $id $((id+1))
done
But this does not produce what I want.
In each loop I want to call a program once, with two files as input (last 2 digits of first file always odd 01, last 2 digits of second file always even 02)
I actually wouldn't use a for loop at all. A while loop that shifts files off is a perfectly reasonable way to do this.
# here, we're overriding the argument list with the list of files
# ...you can do this in a function if you want to keep the global argument list intact
set -- "$FILES"/*.txt ## without these quotes paths with spaces break
# handle the case where no files were found matching our glob
[[ -e $1 || -L $1 ]] || { echo "No .txt found in $FILES" >&2; exit 1; }
# here, we're doing our own loop over those arguments
while (( "$#" > 1 )); do ## continue in the loop only w/ 2-or-more remaining
echo "Processing files $1 and $2" ## ...substitute your own logic here...
shift 2 || break ## break even if test doesn't handle this case
done
# ...and add your own handling for the case where there's an odd number of files.
(( "$#" )) && echo "Left over file $1 still exists"
Note that the $#s are quoted inside (( )) here for StackOverflow's syntax highlighting, not because they otherwise need to be. :)
By the way -- consider using bash's native string manipulation.
stem=${file##*/}
IFS=_ read -r p1 p2 id p_rest <<<"$stem"
number=${id:$(( ${#id} - 2 ))}
output_base="${p1}${p2}${id}"
echo "$id $((10#number + 1))" # 10# ensures interpretation as decimal, not octal
I wrote a bash script that uploads a file on my home server. It gets activated from a folder action script using applescript. The setup is the folder on my desktop is called place_on_server. Its supposed to have an internal file structure exactly like the folder I want to write to: /var/www/media/
usage goes something like this:
if directory etc added to place_on_server: ./upload DIR etc
if directory of directory: etc/movies ./upload DIR etc movies //and so on
if file to place_on_server: ./upload F file.txt
if file in file in place_on_server ./upload F etc file.txt //and so on
for creating a directory its supposed to execute a command like:
ssh root#192.168.1.1<<EOF
cd /var/www/media/wherever
mkdir newdirectory
EOF
and for file placement:
rsync -rsh='ssh -p22' file root#192.168.1.1:/var/www/media/wherever
script:
#!/bin/bash
addr=$(ifconfig -a | ./test)
if ($# -le "1")
then
exit
elif ($1 -eq "DIR")
then
f1="ssh -b root#$addr<<EOF"
list = "cd /var/www/media\n"
if($# -eq "2")
then
list=list+"mkdir $2\nEOF\n"
else
num=2
i=$(($num))
while($num < $#)
do
i=$(($num))
list=list+"mkdir $i\n"
list=list+"cd $i\n"
$num=$num+1
done
fi
echo $list
elif ($1 -eq "F")
then
#list = "cd /var/www/media\n"
f2="rsync -rsh=\'ssh -p22\' "
f3 = "root#$addr:/var/www/media"
if($# -eq "2")
then
f2=f2+$2+" "+f3
else
num=3
i=$(($num))
while($num < $#)
do
i=$(($num))
f2=f2+"/"+$i
$num=$num+1
done
i=$(($num))
f2=f2+$i+" "+$f3
fi
echo $f2
fi
exit
output:
(prompt)$ ./upload2 F SO test.txt
./upload2: line 3: 3: command not found
./upload2: line 6: F: command not found
./upload2: line 25: F: command not found
So as you can see I'm having issues handling input. Its been awhile since I've done bash. And it was never extensive to begin with. Looking for a solution to my problem but also suggestions. Thanks in advance.
For comparisons, use [[ .. ]]. ( .. ) is for running commands in subshells
Don't use -eq for string comparisons, use =.
Don't use < for numerical comparisons, use -lt
To append values, f2="$f2$i $f3"
To add line feeds, use $'\n' outside of double quotes, or a literal linefeed inside of them.
You always need "$" on variables in strings to reference them, otherwise you get the literal string.
You can't use spaces around the = in assignments
You can't use $ before the variable name in assignments
To do arithmetics, use $((..)): result=$((var1+var2))
For indirect reference, such as getting $4 for n=4, use ${!n}
To prevent word splitting removing your line feeds, double quote variables such as in echo "$line"
Consider writing smaller programs and checking that they work before building out.
Here is how I would have written your script (slightly lacking in parameter checking):
#!/bin/bash
addr=$(ifconfig -a | ./test)
if [[ $1 = "DIR" ]]
then
shift
( IFS=/; echo ssh "root#$addr" mkdir -p "/var/www/media/$*"; )
elif [[ $1 = "F" ]]
then
shift
last=$#
file=${!last}
( IFS=/; echo rsync "$file" "root#$addr:/var/www/media/$*" )
else
echo "Unknown command '$1'"
fi
$* gives you all parameters separated by the first character in $IFS, and I used that to build the paths. Here's the output:
$ ./scriptname DIR a b c d
ssh root#somehost mkdir -p /var/www/media/a/b/c/d
$ ./scriptname F a b c d somefile.txt
rsync somefile.txt root#somehost:/var/www/media/a/b/c/d/somefile.txt
Remove the echos to actually execute.
The main problem with your script are the conditional statements, such as
if ($# -le "1")
Despite what this would do in other languages, in Bash this is essentially saying, execute the command line $# -le "1" in a subshell, and use its exit status as condition.
in your case, that expands to 3 -le "1", but the command 3 does not exist, which causes the error message
./upload2: line 3: 3: command not found
The closest valid syntax would be
if [ $# -le 1 ]
That is the main problem, there are other problems detailed and addressed in that other guy's post.
One last thing, when you're assigning value to a variable, e.g.
f3 = "root#$addr:/var/www/media"
don't leave space around the =. The statement above would be interpreted as "run command f3 with = and "root#$addr:/var/www/media" as arguments".
#!bin/sh
i=0;
while read inputline
do
array[$i]=$inputline;
i=`expr $i + 1`
done
j=i;
./a.out > test/temp;
while [$i -gt 0]
do
echo ${array[$i]}
i=`expr $i - 1`;
done
./test > test/temp1;
while [$j -gt 0]
do
echo ${array[$j]}
j=`expr $j - 1`;
done
diff test/temp1 test/temp
What's wrong with the above code? Essentially what it's meant to do is take some input from stdin and then provide the same input to two separate programs and then put their output into another file and then diff them. How come it doesn't work?
I see a few things that could be problems.
First, usually the path to sh is /bin/sh. I would expect the shebang line to be something like this:
#!/bin/sh
This may not be causing an error, however, if you're calling sh on the command line.
Second, the output from your while loops needs to be redirected to your executables:
{
while [ $i -lt $lines ]
do
echo ${array[$i]}
i=`expr $i + 1`;
done
} | ./a.out > test/temp;
Note: I tested this on Mac OS X and Linux, and sh is aliased to bash on both operating systems. I'm not entirely sure that this construct works in plain old sh.
Third, the indexing is off in your while loops: it should go from 0 to $i - 1 (or from $i - 1 to 0). As written in your example, it goes from $i to 1.
Finally, "test" is used as both your executable name and the output directory name. Here's what I ended up with:
#!/bin/sh
lines=0;
while read inputline
do
array[$lines]=$inputline;
lines=`expr $lines + 1`
done
i=0
{
while [ $i -lt $lines ]
do
echo ${array[$i]}
i=`expr $i + 1`;
done
} | ./a.out > test/temp;
i=0
{
while [ $i -lt $lines ]
do
echo ${array[$i]}
i=`expr $i + 1`;
done
} | ./b.out > test/temp1;
diff test/temp1 test/temp
Another way to do what you want would be to store your test input in a file and just use piping to feed the input to the programs. For example, if your input is stored in input.txt then you can do this:
cat input.txt | a.out > test/temp
cat input.txt | b.out > test/temp1
diff test/temp test/temp1
Another approach is to capture stdin like this:
#!/bin/sh
input=$(cat -)
printf "%s" "$input" | ./a.out > test/temp
printf "%s" "$input" | ./test > test/temp1
diff test/temp test/temp1
or, using bash process substitution and here-strings:
#!/bin/bash
input=$(cat -)
diff <(./a.out <<< "$input") <(./test <<< "$input")
What's wrong?
The semi-colons are not necessary, though they do no harm.
The initial input loop looks OK.
The assignment j=i is quite different from j=$i.
You run the program ./a.out without supplying it any input.
You then have a loop that was meant to echo the input. It provides the input backwards compared with the way it was read.
You repeat the program execution of ./test without supplying any input, followed by a repeat loop that was meant to echo the input, but this one fails because of the misassignment.
You then run diff on the two outputs produced from uncertain inputs.
You do not clean up the temporary files.
How to do it
This script is simple - except that it ensures that temporary files are cleaned up.
tmp=${TMPDIR:-/tmp}/tester.$$
trap "rm -f $tmp.?; exit 1" 0 1 2 3 13 15
cat - > $tmp.1
./a.out < $tmp.1 > $tmp.2
./test < $tmp.1 > $tmp.3
diff $tmp.2 $tmp.3
rm -f $tmp.?
trap 0
exit 0
The first step is to capture the input in a file $tmp.1. Then run the two test programs, capturing the output in files $tmp.2 and $tmp.3. Then take the difference of the two files.
The first trap line ensures that the temporary files are removed when the shell exits, or if it receives a signal from the set { HUP, INT, QUIT, PIPE, TERM }. The second trap line cancels the 'exit' trap, so that the script can exit successfully. (You can relay the exit status of diff to the calling program (shell) by capturing its exit status status=$? and then using exit $status.)
If all you want to do is supply the same stdin to two programs you might like to use process substitution together with tee. Assuming you can cat your input from a file (or just using the tee part, if you want it interactive-ish) you could use something like this:
cat input | tee >(./p1 > p1.out) >(./p2 > p2.out) && diff p1.out p2.out
I am fairly new to bash scripting. I can't seem to get the correct value of my counting variables to display at the end of of a while loop in my bash script.
Background: I have a fairly simple task: I would like to pass a text file containing a list of file paths to a bash script, have it check for the existence of those files, and count the number of existing/missing files. I got most of the script to work, except for the counting part.
N=0
correct=0
incorrect=0
cat $1 | while read filename ; do
N=$((N+1))
echo "$N"
if ! [ -f $filename ]; then
incorrect=$((incorrect+1))
else
correct=$((correct+1))
fi
done
echo "# of Correct Paths: $correct"
echo "# of Incorrect Paths: $incorrect"
echo "Total # of Files: $N"
If I have a list of 5 files, 4 of which exist, I expect to get the following output (note the echo command within the while loop):
1
2
3
4
5
# of Correct Paths: 4
# of Incorrect Paths: 1
Total # of Files: 5
Instead, I get:
1
2
3
4
5
# of Correct Paths: 0
# of Incorrect Paths: 0
Total # of Files: 0
What happened to the values of these variables? Google had many suggestions of questionable quality and I think I could get it to work with a little more searching, but a brief explanation of what I'm doing wrong would be very helpful.
This is because you are using the useless cat command with a pipe, causing a subshell to be created. Try it without the cat:
while read filename ; do
N=$((N+1))
....
done < file
Alternatively, if you want to keep the cat for some reason, you can fix your script simply by adding this line before the cat instruction:
shopt -s lastpipe
More generally, sometimes you want to pipe the output of a command. Here's an example that uses process substitution to lint JavaScript files about to be committed by Git and counts the number of files that failed:
# $# glob
git-staged-files() {
git diff --cached -C -C -z --name-only --relative --diff-filter=ACMRTUXB "$#"
}
# $# name
map() { IFS= read -rd $'\0' "$#"; }
declare -i errs=0
while map file; do
echo "Checking $file..."
git show ":$file"|
eslint --stdin --stdin-filename "$file" || ((++errs))
done < <(git-staged-files \*.js)
((errs)) && echo -en "\e[31m$errs files with errors.\e[00m " >&2 || :