Search code examples
bashstdoutstderr

Bash script stderr and stdout


First, i'm not sure if i'm formulating the question correctly. So I have a bash script that delete a user in a system. The problem is, it is showing the err msg before the terminal code. Here. Code:

echo -e "\nCommencing user $UserName removal"
echo -e "Deactivating $UserName shell account ..." "$(chsh -s /usr/bin/false $UserName)"
echo -e "Listing group(s) user: " "$(groups $UserName)"
echo -e "Removing Crontab ... " "$(crontab -r -u $UserName)"

Here is the output:

Commencing user Test2 removal
chsh: unknown user: Test2
Deactivating Test2 shell account ...
groups: Test2: no such user
Listing group(s) user:  
scripts/sudo.sh: line 332: /delete_Test2/crontab.bak: No such file or directory
Saving Crontab ...  

The user to delete is Test2, which is this case "supposedly" does not exist (a different question for a different time). Now, shouldn't the stderr msg display next to the command or below it instead of above it?

Thanks in advance


Solution

  • When the shell executes the line echo -e "Deactivating $UserName shell account ..." "$(chsh -s /usr/bin/false $UserName)", here's the sequence of events:

    1. The shell runs chsh -s /usr/bin/false Test2, with its stdout going to a capture buffer so the shell can use it later.
    2. chsh discover's that "Test2" doesn't exist, and prints "chsh: unknown user: Test2" to its stderr. Since the shell didn't do anything special with its stderr, this goes directly to the shell's stderr, which is your terminal.
    3. chsh exits
    4. The shell takes the captured output (note: stdout, not stderr) from chsh (there wasn't any), and substitutes it into the echo command line, giving echo -e "Deactivating Test2 shell account ..." ""

    Note that the error message gets printed at step 2, but the message about what's supposedly about to happen doesn't get printed until step 4.

    There are several ways to solve this; generally the best is to avoid the whole mess of capturing the command's output, then echoing it. It's pointless, and just leads to confusions like this (and some others you haven't run into). Just run the command directly, and let its output (both stdout and stderr) go to their normal places, in normal order.

    BTW, I also recommend avoiding echo -e, or indeed echo -anything. The POSIX standard for echo says "Implementations shall not support any options." In fact, some implementations do support options; others just treat them as part of the string to be printed. Also, some interpret escape sequences (like \n) in the strings to print, some don't, and some only do if -e is specified (violating the POSIX standard). Given how unpredictable these features are, it's best to just avoid such iffy situations, and either use printf instead (which is more complicated to use, but much more predictable), or (as in your case) just use a separate echo command for each line.

    Also, you should almost always double-quote variable references (e.g. groups "$UserName" instead of groups $UserName), just in case they contain spaces, wildcards, etc.

    Based on the above, here's how I'd write the script:

    echo    # print a blank line
    echo "Commencing user $UserName removal"
    echo "Deactivating $UserName shell account ..."
    chsh -s /usr/bin/false "$UserName"
    
    echo "Listing group(s) user: "
    groups "$UserName"
    
    echo "Removing Crontab ... "
    crontab -r -u "$UserName"