#!/bin/bash # Compilers (consts) CC=gcc CXX=g++ JC=javac # Compiler options (consts) FLAGS="-Wall -Wextra -lm" CFLAGS="${FLAGS} -std=c11" CXXFLAGS="${FLAGS} -std=c++11" JAVAFLAGS="" # The list of supported actions and languages by this command action_list=(help build test output ziptest clean encode tab md html mkex) supported_languages="c|cpp|java" # Global variables: # The version of this software px_version='1.3' px_date='2017-aug-25' # The action asked by the user action= # Options for the asked action options= # The selected programming language, default is 'all' lang= # The target (solution, given) selected by user, default is 'solution' target= # Name of zip file used to compress all test cases zip_name="test_cases.zip" function main { # Start the execution of the program analyze_arguments $@ do_${action} } function analyze_arguments { # Analyze arguments given by user, and updates the global variables # For each command line argument while (( "$#" )); do # Check if it is a global known argument known=false # Global options: help and version if [ "$1" = "--help" ]; then action='help' known=true elif [ "$1" = "--version" ]; then action='version' known=true fi # Check if argument matches a supported action for action_name in ${action_list[@]}; do if [ "$1" = "${action_name}" ]; then # Only the first action is taken, remaining are # considered as options: E.g: 'px help build' if [ -z "${action}" ]; then action="$1" else options="$1" fi known=true fi done # Check if argument is a supported progamming language if [ "$1" = "c" -o "$1" = "cpp" -o "$1" = "java" ]; then lang="$1" known=true fi if [ "$1" = "c++" ]; then lang='cpp' known=true fi # The argument was not recognized, assume it as an option if [ "${known}" = "false" ]; then if [ -z "${options}" ]; then options="$1" else options="${options} $1" fi fi # Move to the next argument shift done set_defaults } function set_defaults { # Default action is build all if [ -z "${action}" ]; then action='build' fi # Default language is all if [ -z "${lang}" ]; then lang='all' fi # Default target is solution: if [ -z "${target}" ]; then target='solution' fi } function do_help { # Action 'help' was called, options should have a command, otherwise # assume general help (same as 'px help help') if [ -z "${options}" ]; then options='help' fi # Call the respective function named do_help_option do_help_${options} } function do_help_help { echo "Programming eXercise tool v${px_version}" echo "Usage: px action [options]" echo echo "actions:" echo " build Compile and build solution from source code (default)" echo " test Check solution against test cases." echo " output Update output files running solution with input files" echo " encode Enforce text files to Unicode and Unix end-of-lines" echo " tab Converts spaces by tabs" echo " md Updates Markdown from html. Requires pandoc" echo " html Updates HTML from Markdown. Requires pandoc" echo " ziptest Compress test cases to a zip file" echo " mkex Create a directory for a new programming exercise" echo " clean Remove object and temporary files" echo " help This help or more information about an action" echo echo "Write 'px help action' to know options for action" check_dependencies } function do_help_build { echo "Builds (compile and link) executables from source code in current directory" echo "Usage: px build [target] [lang]" echo echo "target:" echo " solution Build only solution code (default)" echo " given Build only given code" echo echo "options:" echo " lang Build only code in the specified language: ${supported_languages}" } function do_help_test { echo "Test executables in current directory against all test cases" echo "Usage: px test [target] [lang]" echo echo "target:" echo " solution Test only solution code (default)" echo " given Test only given code" echo " Any executable file to test" echo echo "options:" echo " lang Test only code in the specified language: ${supported_languages}" echo echo "px uses icdiff if installed, otherwise uses diff" } function do_help_output { echo "Updates test case output files using an executable in current directory" echo "Usage: px output [target] [lang]" echo echo "target:" echo " solution Generate output from solution (default)" echo " given Generate output from given code" echo " Any executable file to generate the output" echo echo "options:" echo " lang Generate output using the specified language: ${supported_languages}" } function do_help_encode { echo "Ensures all text files are in Unicode, Unix end-of-lines, and end in new line" echo "Usage: px encode" } function do_help_tab { echo "Replaces indentation spaces by tabs" echo "Usage: px tab [FILES]" echo echo "If FILES are omitted, all supported source code are assumed. If FILES are" echo "specified, be sure do not remove accidentally whitespace in test cases." } function do_help_ziptest { echo "Compress all test case input/output files to a zip file called ${zip_name}" echo "If ${zip_name} already exist, it will be overwritten" echo "Usage: px ziptest" } function do_help_md { echo "Generates or updates Markdown files from all HTML files. Requires pandoc" echo "Usage: px md" } function do_help_html { echo "Generates or updates HTML files from all Markdown files. Requires pandoc" echo "Usage: px html" } function do_help_clean { echo "Remove object and temporary files from current directory" echo "Usage: px clean" } function do_help_mkex { echo "Create a directory for a new programming exercise" echo "Usage: px mkex exercise_name" echo echo "This command must be called inside a section folder. E.g:" echo " cd 1.4_subroutines/" echo " px mkex tower_of_hanoi" } function do_version { echo "px (Programming eXercise tool) v${px_version} [${px_date}]" echo "Jeisson Hidalgo-Cespedes " echo "University of Costa Rica, Computer Science School" echo echo "This is free software with no warranties. Use it at your own risk" } function check_dependencies { if ! hash icdiff 2>/dev/null; then echo echo "Tip: install icdiff to get more readable test differences" echo "[https://www.jefftk.com/icdiff]" fi } function do_build { # Call the build function using the respective compiler and options # according to the selected language, or call all if no language # was specified in arguments if [ "$lang" = "c" -o "$lang" = "all" ]; then build c ${CC} "${CFLAGS}" fi if [ "$lang" = "cpp" -o "$lang" = "all" ]; then build cpp ${CXX} "${CXXFLAGS}" fi if [ "$lang" = "java" -o "$lang" = "all" ]; then build java ${JC} "${JAVAFLAGS}" fi } function build { ext=$1 compiler=$2 flags=$3 # Locate all source files for the given language in current dir for file in $(find . -maxdepth 1 -iname "${target}*.${ext}" | sort); do # Remove initial "./" generated by find file="${file#./}" # Assemble compiler call string passing flags and the file cmd="${compiler} ${flags} ${file}" # Java files do not require '-o executable' option if [ "$ext" != "java" ]; then # Remove extension e.g: if file="solution.c" base="solution" base=$(basename $file) base="${base%.*}" # Append the '-o solution' to the command string cmd="${cmd} -o ${base}" fi # Call the compiler run "${cmd}" done } function run { # Print the command to the stdout, then execute it echo "${1}" eval ${1} } function do_test { # Run solutions against test cases # If we have icdiff in $PATH, we use it, else we use diff if hash icdiff 2>/dev/null; then # icdiff requires to compare files # './solution < inputN.txt > /tmp/px.diff && icdiff /tmp/px.diff outputN.txt' for each N tmp="/tmp/px.diff" run_tests "> $tmp && icdiff -W --no-headers $tmp" rm -f "$tmp" else # './solution < inputN.txt | diff - outputN.txt' for each N run_tests "| diff -u -" fi } function do_output { # Update test case output files running a solution: # './solution < inputN.txt > outputN.txt' for each N run_tests ">" } function run_tests { redirection="$1" name= # C/C++ generate executable files, find them and run each one if [ "$lang" = "c" -o "$lang" = "cpp" -o "$lang" = "all" ]; then # An executable can be specified by name if [ ! -z "${options}" ]; then name="-iname ${options} -a" fi # The way to find executable finds depends on OS if [ "$(uname)" == "Darwin" ]; then files=$(find . ${name} -type f -perm +111 | sort) else files=$(find . ${name} -type f -executable | sort) fi # Run each executable against the test cases for executable in $files; do if [ $(basename $executable) != "px" ]; then test_solution "${executable}" "${redirection}" fi done fi # Java produces .class files, we assume Solution and run it if [ "$lang" = "java" -o "$lang" = "all" ]; then # An executable can be specified by name, default is test all java solutions if [ -z "${options}" ]; then for class_file in $(find . -iname 'Solution*.class' | sort); do # Remove the '.class' to call 'java Solution' without .class class_name=$(basename $class_file) class_name="${class_name%.*}" test_solution "java ${class_name}" "${redirection}" done else if [ -f "${options}.class" ]; then test_solution "java ${options}" "${redirection}" else echo "error: could not find java solution: ${options}" 1>&2 fi fi fi } function test_solution { executable="$1" redirection="$2" # Run $executable against each test case. Find inputN.txt files for input in input*.txt; do # Assemble "outputN.txt" extracting the number from "inputN.txt" number=${input//[^0-9]/} output=output${number}.txt # Run the executable to test or update the ouput run "${executable} < ${input} ${redirection} ${output}" done } function do_encode { # For all text files (not binary) in current directory for file in $(find . -type f -exec grep -Iq . {} \; -and -print | sort); do to_unicode "${file}" to_unix_eol "${file}" eol_at_eof "${file}" done } function to_unicode { file="$1" # Get the file encoding using "file -I" command if [ "$(uname)" == "Darwin" ]; then from_enc=$(file -I "${file}") else from_enc=$(file -i "${file}") fi # Etract the last word from 'charset=xxx' response from 'file -I' from_enc=${from_enc##*=} # Ignore lowercase/uppercase shopt -s nocasematch # If file encoding is not Unicode/ASCII e.g: Latin-1 we must convert if [ "$from_enc" != "utf-8" -a "$from_enc" != "us-ascii" ]; then echo "${file}: $from_enc -> utf-8" # Convert encoding to Unicode saving it to temporary file iconv -f $from_enc -t utf-8 < "${file}" > "${file}.utf8" # If success if [ $? -eq 0 ] ; then # Move temporary file overwritting the original one mv "${file}.utf8" "${file}" else # Remove invalid temporary file rm -f "${file}.utf8" fi fi } function to_unix_eol { file="$1" # Command 'file' reports MS-DOS's CRLF end-of-lines if file ${file} | grep "CRLF" > /dev/null 2>&1; then echo "${file}: to unix end-of-lines" # Remove the '\r' character from file, using a temporaral file tr -d "\r" < ${file} > ${file}.tmp mv ${file}.tmp ${file} fi } function eol_at_eof { file="$1" # If the last line of file is not empty if [ ! -z "$(tail -c 1 "${file}")" ]; then # Append an empty line to the file echo "${file}: new line at end" echo >> "${file}" fi } function do_tab { # If no files are given, we assume all supported source code files if [ -z "${options}" ]; then options=(*.{c,cpp,h,java}) else # Convert string to array options=($options) fi # For each file for file in "${options[@]}"; do # If file exists if [ -f "$file" ]; then # Convert spaces to tabs, save result to temporary file unexpand -t 4 "${file}" > "${file}.tab" # If success and result is different than original if [ $? -eq 0 ]; then if ! cmp --silent "${file}" "${file}.tab"; then # Move temporary file overwritting the original one echo "${file}: spaces to tabs" mv "${file}.tab" "${file}" else # Remove invalid temporary file rm -f "${file}.tab" fi fi fi done } function do_md { # Convert all problem.xx.html files to problem.xx.md convert_problem html md } function do_html { # Convert all problem.xx.md files to problem.xx.html convert_problem md html } function convert_problem { from_notation=$1 to_notation=$2 # For each file in from notation (e.g: html) for from_file in *.${from_notation}; do # Remove extension, e.g: problem.es.html -> problem.es name=$(basename $from_file) name="${name%.*}" # Convert to target notation using pandoc run "pandoc ${from_file} -o ${name}.${to_notation}" done } function do_mkex { exercise_name="${options}" common_dir="../common" if [ -d "${common_dir}/c_invented_exercise/" ]; then mkdir "${exercise_name}" if [ $? -ne 0 ] ; then exit 1 fi # If current directory starts with 1.x, is a C exercise c_cpp="${lang}" if [ -z "${c_cpp}" -o "${c_cpp}" = "all" ]; then c_cpp='cpp' if [[ ${PWD##*/} == 1* ]] ; then c_cpp='c' fi fi cd "${exercise_name}" #touch input01.txt #touch output01.txt touch problem.es.html #touch tests.txt echo "**Entrada de ejemplo**:" >> problem.es.md echo >> problem.es.md echo "**Salida de ejemplo**:" >> problem.es.md echo >> problem.es.md ln -s ../../common/Makefile cp -p ../../common/given.${c_cpp} . cp -p ../../common/given.${c_cpp}.pro . cp -p ../../common/given.${c_cpp} solution.${c_cpp} cp -p ../../common/solution.${c_cpp}.pro . cp -p ../../common/Solution.java . cd .. else echo "px mkdir must called from a section directory" 1>&2 echo "call 'px help mkdir' for more information" 1>&2 fi } function do_ziptest { # If and old .zip file does exist, remove it if [ -f ${zip_name} ]; then run "rm -f ${zip_name}" fi # Compress the test cases run "zip -9 ${zip_name} input*.txt output*.txt" } function do_clean { # Remove only executable files that begin with 'solution' or 'given' for file in $(find . -type f -executable -and -regex '\./\(solution\|given\).*$' | sort); do run "rm -f ${file}" done # Remove object code run "rm -rf *.o *.class *.dSYM *.zip" } main $@