1

This is code for finding makefile in bash shell. Given IFS=$'/\n'

    echo "Testing $user_directory"

    cd $user_directory
    unzip -q '*.zip'
    TEST_ERROR=0

    # find filepath to makefiles
    proj_list=$(find -iname "makefile*" -exec dirname {} \;)

    #check number of directories
    num_proj=()
    **while IFS= read -r -d $'\n'; do
            num_proj+=("$REPLY")
    done < <(find -iname "makefile*" -exec dirname {} \; | sort)**

Can you explain the while loop with bold(**) line?

4
  • 1
    Does this help unix.stackexchange.com/questions/184863/… Commented Aug 25, 2020 at 21:06
  • 1
    Learn to disassemble code to see what is it doing. Execute just find -iname "makefile*" -exec dirname {} \; | sort. Now put that back, but change the inside of the loop to just echo "|$REPLY|". Goto the bash documentation to see what the options -r -d $'\n' do for the read command. You'll need to get used to this sort of research with any programming tool you use. Good luck. Commented Aug 25, 2020 at 21:10
  • 1
    bash doc. Good luck. Commented Aug 25, 2020 at 21:11
  • 2
    See: explainshell.com Commented Aug 25, 2020 at 21:33

2 Answers 2

2

There is a lot going on in that loop. However, Bash is exceptionally well documented, and the manuals have a lot of really useful information. Here are snippets pulled directly from the manual that provide the answers you are looking for.


Everything in this section is taken from this page:

https://www.gnu.org/software/bash/manual/bash.html

3.2.4.1 Looping Constructs

while

The syntax of the while command is:

while test-commands; do consequent-commands; done

Execute consequent-commands as long as test-commands has an exit status of zero. The return status is the exit status of the last command executed in consequent-commands, or zero if none was executed.

5.1 Bourne Shell Variables

IFS

A list of characters that separate fields; used when the shell splits words as part of expansion.

4.2 Bash Builtin Commands

read

One line is read from the standard input, or from the file descriptor fd supplied as an argument to the -u option, split into words as described above in Word Splitting, and the first word is assigned to the first name, the second word to the second name, and so on.

The characters in the value of the IFS variable are used to split the line into words using the same rules the shell uses for expansion (described above in Word Splitting).

-d delim

The first character of delim is used to terminate the input line, rather than newline. If delim is the empty string, read will terminate a line when it reads a NUL character.

-r

If this option is given, backslash does not act as an escape character. The backslash is considered to be part of the line. In particular, a backslash-newline pair may not then be used as a line continuation.

3.5.7 Word Splitting

If IFS is unset, or its value is exactly <space><tab><newline>, the default, then sequences of <space>, <tab>, and <newline> at the beginning and end of the results of the previous expansions are ignored, and any sequence of IFS characters not at the beginning or end serves to delimit words.

3.6.1 Redirecting Input

Redirection of input causes the file whose name results from the expansion of word to be opened for reading on file descriptor n, or the standard input (file descriptor 0) if n is not specified.

The general format for redirecting input is:

[n]<word

3.5.6 Process Substitution

Process substitution allows a process’s input or output to be referred to using a filename. It takes the form of

<(list)


So with that relevant information, it is possible to piece together what this loop is doing:

    while IFS= read -r -d $'\n'; do
            num_proj+=("$REPLY")
    done < <(find -iname "makefile*" -exec dirname {} \; | sort)

First, we should start with this command, since it provides the input to your read loop:

find -iname "makefile*" -exec dirname {} \; | sort

find is a program, and not a built-in, so the documentation for it is separate from the Bash documents. Feel free to look it up if you're interested, but in summary, this find command prints out all directories containing a file starting with the string "Makefile" (without case-sensitivity), one directory per line.

Now that we know the input is a list of directories, that brings us to the loop itself:

    while IFS= read -r -d $'\n'; do
            num_proj+=("$REPLY")
    done

This construction will cause the inner loop to run one time for every line passed into it. Since we know that every line is one directory, the line num_proj+=("$REPLY") will run once for every directory.

All this combines to create a loop that appears to count the number of directories with a Makefile in it, to give an approximate count of the number of projects in sub-directories from where the program is run.

Now that it's clear what the program is doing as written, I think this can be rewritten to be both simpler and clearer. I would probably write this as follows:

    IFS=$'\n' # We want to break up the input by only newlines, not whitespace.
    for dir in $proj_list; do
        num_proj+=("$REPLY")
    done
    unset IFS # always clean up IFS when you're done with it.

Much easier to follow, no?

Sign up to request clarification or add additional context in comments.

1 Comment

Good stuff, and defiintely an up-vote, but unset(ing) IFS is never a good idea. On solution is to save and restore after the non-default value is no longer needed, OR to run the whole thing in a sub-shell, ie ( IFS=... ; while ; ... ; done ), except to get a variable returned, you need to do var=$( IFS=...; while ; ... ; done) (and I may not have that competely right!). Good luck to all.
0

I agree with @Swiss's breakdown. However, $proj_list is an array so you should reference it as ${proj_list[@]}. Also, the IFS defaults to $'\n', so I don't think you want to set it before running. I believe this is better:

for dir in "${proj_list[@]}"; do
    num_proj+=("$REPLY")
done

2 Comments

proj_list is just a string in the provided program. Syntax to make it an array would be: proj_list=( $(find -iname "makefile*" -exec dirname {} \;) ) (note the extra parens)
I don't think it would be correct to turn proj_list into an array either, since it would break if there is any whitespace in a directory name.

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.