7
\$\begingroup\$

This is a simple bash script that I use to compile and run single C++ files for coding competitions.

Features:

  • Detects if there is a corresponding .in file next to it, and if so uses that as stdin
    • e.g. if the file problem1.cpp and problem1.in are in the same directory, the script will redirect stdin from problem1.in
  • Compile each file to a temp directory so it doesn't clutter the working directory
  • Configurable g++ warning flags as needed

Note: \033[32m and \033[0m are terminal color codes that make the text green

#!/bin/bash
# Compiles and runs .cpp code
# if there exists an .in file next to the .cpp file
# it will use that as input

if [ -z $1 ]; then
  echo -e "Please choose an input file"
  exit 1
fi

FILE="$1"
FILE_IN="${FILE%.*}.in"

clear
echo -e "\033[32mCompiling...\033[0m"
TMPFILE=$(mktemp /tmp/run-cpp.XXXXXXXXXX)
WARNING_FLAGS="-Wuninitialized -Wmaybe-uninitialized"
g++ $FILE -std=c++17 $WARNING_FLAGS -O3 -o $TMPFILE

if [ $? -eq 0 ]; then
  echo -e "\033[32mRunning...\033[0m"
  if [ -f $FILE_IN ]; then
    $TMPFILE < $FILE_IN
  else
    $TMPFILE
  fi
  ERROR=$?
fi
rm $TMPFILE 2> /dev/null
exit $ERROR

Usage: ./runcpp.sh /path/to/my-cpp-file.cpp

\$\endgroup\$
4
+50
\$\begingroup\$
#!/bin/bash

I don't see why you're using Bash for this - there should be no problem using standard POSIX shell.

if [ -z $1 ]; then

I would recommend "$1" there, even though extra arguments will cause [ to return false for other reasons. Passing as a single argument won't emit any error messages.

  echo -e "Please choose an input file"

echo -e isn't portable, and isn't necessary here anyway.

FILE="$1"
FILE_IN="${FILE%.*}.in"

Avoid all-caps names for variables in your own program - these are generally reserved for environment variables which change programs' behaviour.

clear

I think that's a bit rude - some of us like to be able to compare results against the previous run. If I want to clear before I run, I can type that easily.

echo -e "\033[32mCompiling...\033[0m"

Don't embed terminal-specific escape codes like that! Even though most recent terminals support the ANSI escapes, there are others - and if you redirect to file, you don't want it littered with control characters. Use tput to generate the correct escapes for your $TERM (and slightly more readable code).

TMPFILE=$(mktemp /tmp/run-cpp.XXXXXXXXXX)

That's not a very descriptive variable name. It's more informative to say what it's for (the executable to create). And why hard-code /tmp as the directory? Prefer $TMPDIR if set (perhaps systems with per-user temp directories, or with a choice of fast or large temporary storage).

WARNING_FLAGS="-Wuninitialized -Wmaybe-uninitialized"

That's quite a lax set of warnings. If you care about the quality of your source, add a few more. I suggest -Wall -Wextra -Wwrite-strings -Wno-parentheses -Wpedantic -Warray-bounds -Weffc++. If you only care about performance, then perhaps -Wall -Wextra -Wno-parentheses -Warray-bounds might be sufficient.

Since we use this variable only once, perhaps we should inline its use, thereby not triggering a Shellcheck warning where we (correctly) expand it without quotes.

g++ $FILE -std=c++17 $WARNING_FLAGS -O3 -o $TMPFILE

"$FILE" here, too. I'd write "$TMPFILE" even though we constructed it to be a safe name.

if [ $? -eq 0 ]; then

Anti-pattern - just use the preceding command directly after if. Or, enable the -e flag of the shell, to just exit if the compilation fails.

  echo -e "\033[32mRunning...\033[0m"

tput again.

  if [ -f $FILE_IN ]; then

Quotes again.

    $TMPFILE < $FILE_IN
  else
    $TMPFILE
  fi

As we don't use stdin, we could just redirect stdin using exec, and not need two different commands.

  ERROR=$?
fi
rm $TMPFILE 2> /dev/null

Why didn't we remove the temporary file if we exited early?

exit $ERROR

We wouldn't need to store the error if we made removing the temp-file an exit trap.


Modified code

#!/bin/sh
# Compiles and runs C++ source code. A corresponding file
# ending with .in will be used as input, if present

set -eu

if [ $# -ne 1 ] || [ -z "$1" ]
then
  echo "Usage: $0 SOURCE"
  exit 1
fi

executable=$(mktemp -t run-cpp.XXXXXXXXXX)
trap 'rm $executable' EXIT

green=$(tput setaf 2)
normal=$(tput sgr0)

echo "${green}Compiling...${normal}"
g++ -o "$executable" -std=c++17 -O3                     \
    -Wall -Wextra -Wwrite-strings -Wno-parentheses      \
    -Wpedantic -Warray-bounds -Weffc++                  \
    "$1"

echo "${green}Running...${normal}"
input=${1%.*}.in
if [ -f "$input" ]
then exec <"$input"
fi

"$executable"

You might also be interested in my approach to a similar situation, which uses Make rather than shell.

\$\endgroup\$
2
\$\begingroup\$

The other review makes most of the points I would have, so I'll just add three more points.

Don't ignore the return value of mktemp

For security reasons, mktemp will refuse to create a file if the name it has chosen already exists. This is to avoid security flaws. You will probably be saved because /tmp/ should always have the sticky bit set, preventing users other than the owner of the file from deleting it. Still, I'd recommend a construction like this instead:

TMPFILE=$(mktemp /tmp/example.XXXXXXXXXX) || exit 1

Another option would be to omit the output file name and just allow the compiler to create a.out in the local directory, bypassing the issue entirely as well as shortening your script.

Use the result directly instead of testing $?

The code includes this:

g++ $FILE -std=c++17 $WARNING_FLAGS -O3 -o $TMPFILE
if [ $? -eq 0 ]; then

But this could be made a little more direct:

if g++ $FILE -std=c++17 $WARNING_FLAGS -O3 -o $TMPFILE ; then

Or perhaps better, see the next suggestion.

Consider using functions

I'd suggest it may be useful to create functions, such as run and compile to make the script a little easier to understand. For example:

compile() {
    WARNING_FLAGS="-Wuninitialized -Wmaybe-uninitialized"
    g++ "$1" -std=c++17 $WARNING_FLAGS -O3 -o "$2"
}

if compile "$FILE" "$TMPFILE"; then
 
\$\endgroup\$
2
  • 1
    \$\begingroup\$ Also, this alternative approach uses Python. \$\endgroup\$ – Edward Dec 11 '20 at 15:03
  • \$\begingroup\$ I did mention that testing $? is an antipattern. But your explanation is more helpful, since it shows the fix, whereas mine is hidden in the rewritten script. (In fact, it's not even there, due to set -e). \$\endgroup\$ – Toby Speight Dec 11 '20 at 16:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.