7
\$\begingroup\$

Not long ago, I was asked about how many lines of code I have written in C++ and could not answer that question. For this reason and to become more familiar with Bash shell scripting, I wrote a small system to monitor the lines of code written in a specific subdirectory.

Currently, I count the lines of code at 5 seconds interval and write the difference (if favorable) to a file.

As mention above, I am not familiar with bash scripting and hope to get some helpful feedback.

I would appreciate the advice, especially in the following areas:

  • Code style (readability, naming conventions)
  • Efficiency (how to avoid unnecessary complexity)
  • How can I prevent a long list of extension as used currently in my approach (-name "*.py" -o -name "*.cpp" -o -name "*.h"). Can I do something like [py, cpp, h, ...] instead?
count_lines () {
    N="$(find ./ -name "*.py" -o -name "*.cpp" -o -name "*.h" | \
    xargs grep -cve "^\s*$" | grep -oP '(?<=:).*' | \
    awk '{s+=$1} END {print s}')"
    echo "$N"
}

FILE_NAME="counter.log"
touch $FILE_NAME
SUM=0

while true; do 
    NUM1=$(count_lines) 
    sleep 5;
    NUM2=$(count_lines) 
    DIFF=$((NUM2-NUM1))
    if [ "$DIFF" -gt 0 ]; then
        SUM=$((SUM+DIFF))
    fi
    echo "${SUM} ${NUM2}" >> $FILE_NAME
done
\$\endgroup\$
1
  • \$\begingroup\$ I think this script is using an inappropriate language for the job, though I'm biased. I have a personal rule that I would recommend following: The set of acceptable bash scripts consists exclusively of scripts with precisely one line, to start your choice of python, ruby, go, swift, or whatever other "real" language you fancy. Seriously, the value of N includes: xargs, 3 pipes, an in-line AWK program, 2 calls 2 grep, and an entire clusterf*** of commas, dots, dashes and other sigils. I absolutely guarantee you that you won't remember what all those flags mean in 6 months time. \$\endgroup\$
    – Alexander
    Dec 19, 2019 at 22:52

3 Answers 3

5
\$\begingroup\$

This isn't any shorter, but it is more dynamic:

exts=( py cpp h )

count_lines() {
    local find_args=( -false )
    for ext in "${exts[@]}"; do
        find_args+=( -o -name "*.$ext" )
    done

    find ./ "${find_args[@]}" -print0 |
    xargs -o grep -cve "^\s*$" |
    grep -oP '(?<=:).*' |
    awk '{s+=$1} END {print s}'
}

The extensions reside in a bash array that can live in the global scope. It's one place to store your list of interesting files.

In count_lines you can build up an array of arguments to pass on to find. I'm using -false as the first one so that it's simple to add -o -name "*.something in the loop.

You don't need to capture the command output only to print it on the next line. Just let the commands print to stdout directly.

I added -print0 to find and -0 to xargs. These options work well together to be able to handle any filename, regardless of any whitespace in the name.

Note you don't need to use line continuations when the line ends with a pipe (or with a && or ||) -- bash understands that the pipeline continues on the next line.

About your awk command: if your goal is to count lines, don't you want {s+=1} or {s++} instead of {s+=$1}? The latter will only sum any filenames that look like numbers:

$ touch 5 8 a b c
$ ls | awk '{sum += $1; count++} END {print sum, count}'
13 5

\$\endgroup\$
4
\$\begingroup\$

Unnecessary capturing of output

It's strange in count_lines to capture the output of find only to echo it.

Instead of:

N="$(pipeline)"
echo "$N"

This is the same thing, with fewer processes:

pipeline

Complicated counting of lines

You used a grep to count lines, then another grep to remove other junk to keep only counts, then an awk to sum counts. It would be a lot simpler to use wc.

Instead of:

xargs grep -cve "^\s*$" | grep -oP '(?<=:).*' | \
awk '{s+=$1} END {print s}'

You could write:

xargs grep -ve "^\s*$" | wc -l

Calling count_lines more than necessary?

The while loop calls count_lines, waits for 5 seconds, then calls count_lines again, and prints some stats, then starts over again. The printing of stats is a fast operation, so some count_lines calls look wasteful.

It would be better to call count_lines only once per iteration, and compare values to the call in the previous iteration. (This also means you'll need to call it once before the loop too, to initialize.)

Other minor issues

Capitalized variable names are not recommended in scripts.

Always double-quote variables used in command line arguments, for example touch "$filename" instead of touch $filename.

; at the end of a line is unnecessary.

Instead of diff=$((a - b)) you can write ((diff = a - b)). Btw, ((sum += diff)) also works.

\$\endgroup\$
3
\$\begingroup\$

Open the toolbox

The hard part may be accomplished using watch, the filtering may be done using sed.

#!/bin/bash -eu
# Number of lines of code in a directory

files=(*.py *.h *.cpp)

shopt -s nullglob

if [[ $# -ne 1 ]]; then
   echo "usage: wc_code directory"
   exit 1
fi

if [[ ! -d $1 ]]; then
  echo "no such directory: $1"
  exit 1
fi

cd "$1" && sed '/^[[:space:]]*$/d' ${files[@]} | wc -l

wc_code is the script name located in the search path. /home/user/dir is a directory containing source files.

prompt% watch wc_code /home/user/dir

Note: the sample code may be improved, it may also depend on your preferences. For instance, the previous script may be replaced by watch 'sed /^[[:space:]]*$/d *.py *.cpp *.h | wc -l'.

See Also

My personal opinion is that we can easily achieve the same task in broad stokes. The number of lines of source code is not necessarily a determining criterion.

watch wc -l *.py .*cpp *.h

Retrospective

If your task is specific it is probably better to develop a new program than to create a shell script because shell utilities can perform common tasks.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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