1

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?

5
  • 3
    Please replace all images with its text. Commented Jan 30, 2021 at 13:04
  • 1
    Now you replaced the images with text, but there is a different script in the image, compared to the text.... Commented Jan 30, 2021 at 13:44
  • Sorry, the second image comes from my VM, it is not too convenient to replace it with text. But it is the same as the first script except I added the correct global variable and some output if compliant. Commented Jan 30, 2021 at 13:56
  • Yoy can put at beginning of script shopt -s lastpipe which allows to run last command of pipe in the current shell. Commented Jan 30, 2021 at 14:32
  • May I know what does the shopt -s lastpipe command do? Commented Jan 30, 2021 at 14:43

2 Answers 2

1
  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 sedawk 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?
Sign up to request clarification or add additional context in comments.

Comments

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" 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.