skip to Main Content

In the following script, I am using bash to check whether users own their home directories as part of the CIS CentOS 8 Benchmark (6.2.8).

#!/bin/bash
grep -E -v '^(halt|sync|shutdown)' /etc/passwd | awk -F: '($7 != "'"$(which nologin)"'" && $7 != "/bin/false") { print $1 " " $6 }' | while read user dir; do
 if [ ! -d "$dir" ]; then
        echo "The home directory ($dir) of user $user does not exist."
 else
        owner=$(stat -L -c "%U" "$dir")
        if [ "$owner" != "$user" ]; then
                echo "The home directory ($dir) of user $user is owned by $owner."
        fi
 fi
done

I am trying to print something if there are no errors using a global variable. The following is my attempt at it:

correct=true
grep -E -v '^(halt|sync|shutdown)' /etc/passwd | awk -F: '($7 != "'"$(which nologin)"'" && $7 != "/bin/false") { print $1 " " $6 }' | while read user dir; do
 if [ ! -d "$dir" ]; then
        echo "The home directory ($dir) of user $user does not exist."
        correct=false
 else
        owner=$(stat -L -c "%U" "$dir")
        if [ "$owner" != "$user" ]; then
                echo "The home directory ($dir) of user $user is owned by $owner."
                correct=false
        fi
 fi
done
if [ "$correct" = true ]; then
        echo "Non-compliance?: No"
        echo "Details: All users own their home directories."
        echo
fi

However, the global variable, correct, will not change regardless of what happens in the while loop because it is in multiple sub-shells.
I read up about this and noticed people using "here strings" so that the while loop will not be in a sub-shell. However, for my case I have multiple pipes (possibly might even add more for other scripts), so I don’t really know how to make it do what I want here.

How can I get results information out of a loop
so I can display summary information after the loop completes
when the loop is executed in a sub-shell?

2

Answers


  1. This How is return handled in a function while loop?, and some rewriting led to this script, which can return false:

    #!/bin/bash
    
    tmp=/tmp/$$
    grep -E -v '^(halt|sync|shutdown)' /etc/passwd | 
       grep -E -v '/bin/false$|nologin$' | 
       awk -F: '{ print $1,$6 }' > $tmp
    
    correct=true
    
    while read user dir; do
      #echo $user $dir
      if [ ! -d $dir ];
      then
         echo "Dir ($dir) does not exist"
         correct=false
      fi
      owner=$(stat -L -c "$user" "$dir")
      if [ "$owner" != "$user" ];
      then
         echo "home directory for user ($user) is owned by ($owner)"
         correct=false
      fi
    done < $tmp
    
    rm $tmp
    
    echo "CORRECT: $correct"
    
    Login or Signup to reply.
    1. Your code/design fails on an edge case:
      user Horatio Altman might have a username of haltman
      Your code would ignore him since his name begins with halt
      You should use a regular expression
      of '^(halt|sync|shutdown):'
      Note the colon at the end — this will match only lines that have halt,
      sync or shutdown as the first field in the :-delimited file.
    2. awk is a very powerful program. 
      You almost never need to combine it with anything like grep or sed
      awk can do (pretty much?) anything they can do. 
      In your script, we can eliminate the grep
      and put the halt / sync / shutdown logic into awk:

      awk -F: '($1 != "halt" && $1 != "sync" && $1 != "shutdown" && $7 != "'"$(which nologin)"'" && $7 != "/bin/false") { print $1 " " $6 }' /etc/passwd |
              while read user dir; do
                        ︙
      

      Since $1 means everything up to the first colon (since we have -F:)
      and we are doing simple string equality checking
      (rather than regexp matching),
      we don’t need the ^ at the beginning or the : at the end. 
      Also note that we can add an Enter after a |
      without needing a backslash.

    3. The triple quote character is messy; it’s hard to read,
      and easy to get wrong, especially when you edit the script a year from now. 
      A cleaner way to inject information into an awk script is to use a variable:

      awk -F: -v nl="$(which nologin)" 
                  '($1 != "halt" && $1 != "sync" && $1 != "shutdown" && $7 != nl && $7 != "/bin/false") { print $1 " " $6 }' /etc/passwd |
              while read user dir; do
                        ︙
      

      Here I created an awk variable called nl
      with the value of $(which nologin),
      and then used that variable in the comparison against $7
      (Also I broke that very long line with a backslash, for readability.)

    4. OK, now I’ll start really answering the question. 
      As you understand, the problem with your script is that
      it sets the correct variable inside a subshell
      and then tries to access it outside the subshell. 
      One solution is to move the statement(s) that access the variable
      into the subshell. 
      You might think that this is infeasible,
      since the subshell is the while loop,
      and you don’t want to print the success message inside the loop. 
      The trick is to force a larger subshell:

      correct=true
      awk -F: -v nl="$(which nologin)" 
                  '($1 != "halt" && $1 != "sync" && $1 != "shutdown" && $7 != nl && $7 != "/bin/false") { print $1 " " $6 }' /etc/passwd |
          (
              while read user dir; do
                      if [ ! -d "$dir" ]; then
                              echo "The home directory ($dir) of user $user does not exist."
                              correct=false
                      else
                              owner=$(stat -L -c "%U" "$dir")
                              if [ "$owner" != "$user" ]; then
                                      echo "The home directory ($dir) of user $user is owned by $owner."
                                      correct=false
                              fi
                      fi
              done
              if [ "$correct" = true ]; then
                      echo "All users own their home directories."
              fi
          )
      

      I added parentheses to force a subshell that encompasses
      the while loop and the if-then statement. 
      I put the parentheses on separate lines for readability;
      you can move them onto the preceding line or the following line
      if you want to minimize your line count. 
      (The next code block demonstrates this.)

    5. But what if you want to use the value of $correct
      at some point much later in the script? 
      You don’t want to move large, unrelated chunks of code into the subshell
      just so you can use this trick. 
      Well, my next trick is to pass information out of the subshell
      by using its exit status:

      #!/bin/bash
      correct=true
      awk -F: -v nl="$(which nologin)" 
                  '($1 != "halt" && $1 != "sync" && $1 != "shutdown" && $7 != nl && $7 != "/bin/false") { print $1 " " $6 }' /etc/passwd |
              (while read user dir; do
                      if [ ! -d "$dir" ]; then
                            ︙
                      fi
              done
              if [ "$correct" = true ]; then
                      exit 0
              else
                      exit 1
              fi)
      if [ "$?" = 0 ]; then
              correct=true
      else
              correct=false
      fi
                ︙                         # Possibly much later in the script.
      if [ "$correct" = true ]; then
              echo "All users own their home directories."
      fi
      

      In theory, exit codes can range from 0 to 255,
      although it’s best to limit yourself to the 0-125 range. 
      See What is the min and max values of exit codes in Linux?

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search