Linux Shell Scripting
The course is part of this learning path
In this course, we'll cover a range of topics designed to help you enhance your Linux scripts. We'll start off by looking at case statements, which are used to make a decision based on the value of a given variable. We'll cover functions before moving and then move on to how to process command-line options using the shell built-in getopts.
In the second part of the course, we'll look at managing users including how to disable, delete, and archive users on a Linux system. We'll then do a walkthrough exercise showing you how to delete a user, which you can follow along with.
This course is part of the Linux Shell Scripting learning path. To follow along with this course, you can download all the necessary resources here.
- Learn about case statements and functions to make your scripts more efficient
- Process command line options using getopts
- Manage users in Linux
- Anyone who wants to learn Linux shell scripting
- Linux system administrators, developers, or programmers
To get the most out of this course, you should have a basic understanding of the Linux command line.
In the previous exercises you have been writing scripts to do things like add users. Well, now it's time to do the opposite. We're gonna write a script that deletes users and actually this particular script, not only deletes users, it can disable them and it can archive a user's home directory. So the first thing of course we need to do is come up with a little requirements document, if you will, and figure out what in the world do we want our script to do. And here are the things that we want the script to do and how we want it to operate and what we want it to look like. Well, the first thing we decided on was that we want to make sure that this script is called "disable-local-user.sh." Since we're doing some system administration type stuff like deleting users or disabling them, we need to make sure that whoever's executing this script has root privileges or super user privileges. So we're gonna put a check for that in the top of our script. We also wanna provide a usage of statement, much like you would find in a man page if the user that's executing the script does not supply an account name on the command line. And then of course, we're gonna follow good Linux programming conventions and exit our script with a non-zero exit status. We also decided to use disable as the default action for this script. And we also decided that we can let the users decide if they wanna do something more destructive, such as delete an account or remove the user's home directory. So we've given them a few different options. Again, the -d will delete an account instead of disabling it, - r is going to remove the home directory and a -a will create an archive of the home directory associated with the account and then store it in the /archives directory. Now, by the way, this is just kind of an arbitrary decision that I made. I'm gonna put these things in /archives. There is no standard for such a directory. This is not gonna be a directory that you're gonna find by default on a newly installed Linux system. It's not part of the Linux file system hierarchy, it's just a folder that I decided to use. In that respect what we need to do is just make sure that that directory exists, and if it doesn't exist we're gonna have our script to create it. Now, if a user does something interesting like supply an option that we don't account for then we're going to give them that usage statement and exit as well. We've also decided to make it a little bit easier on the end users that are gonna be running this script. They can supply, you know, one user, two users or however many users they want at the command line. So if they want to delete seven users, well, they can supply all seven users at once on the same command line and this script will simply loop through them all and perform the exact same action against all those users. So if a user supplies the -d option and seven accounts, well, it's gonna delete all seven of those accounts. Speaking of deleting accounts, we don't want someone to do something interesting like delete the root account and make our system unusable. Along this logic, we don't want to let people who are not system administrators do things like disable any system account. We know that typically system accounts have a user ID less than 1000, so let's just put a little check for that in our script and we'll exit saying, hey, we're not gonna do that if a system account needs to be deleted, well, give that task over to a system administrator and let them do it. If we encounter an error along the way, either deleting or disabling or archiving the account or creating a directory or what have you, we're going to tell the user that we encountered a problem and then exit the script. Also, the last thing we wanna do is just make sure that we display the username and any actions that we took against that account or username for the user that executed the script. So that pretty much does it for our requirements. Now, let's get to scripting. Here I have a terminal open on my local system and I'm going to go into the shell class folder. We're still working on the local users project and I'm going to bring up the virtual machine and log into it. Now we're going to our shared folder, a /vagrant. And again, we decided to name this script disable-local-user.sh. Okay, it goes without saying we're going to supply a shebang line. And then we'll just give a comment about what the script is going to do. So we know that we're going to be executing some commands that require root privileges. So let's just go ahead and check that right away. So if the user ID is not equal to zero then they are not root. This is an error message. So we're gonna send this to standard error and then we're going to exit with a non zero exit status. We also want to parse the command line options. So we'll do that next. We know that we can use getopts in a while loop. We'll specify our op strain here, the options that we're going to accept. And we're going to accept d to delete, r to remove, a to archive, and we'll use the variable option. We'll check that option in the case statement here. So if someone supplies d let's set a variable called delete_user and we'll just set that to true. And then later on in our script, we'll check to see if delete user is true and if it is then, well, we'll delete the user. Here I'm going to do something slightly different. If you recall the user del command allows you to specify -r option which removes the user's home directory. So what we're going to do here is use this remove option variable in the user del statement when we get to that in the script. So if this is set to nothing or if the option is never supplied, this -r option, then remove underscore option is going to evaluate to an empty string and nothing will be substituted in its place, and so then user del will be executed without this -r option. Now, on the other hand, if someone does specify -r, then user del will be deleted with -r. Now it's gonna make total sense when you see it in a minute. I just want to talk about that for just a second. We'll do something similar here that we did with the delete user option which is if someone gives us a -a, we'll just check for this archive being true later in the script. And if they give us any other option, then we're going to tell them how to use our script and exit. Now, my case statement here is looking pretty nice and compact, and I think I'm going to use a function for displaying this usage statement and then exiting the script. Now, another argument for creating a function like this is that we're gonna have to do this same thing multiple times in the script because we're gonna have some other checks and then we're going to give them a usage statement and exit. So let's go back to the top of our script here and go ahead and create our function. As you remember, dollar sign zero is always the name of the script, not the name of the function like you may intuit, that's not what happens with dollar signs zero, it's always the name of the script. So we can safely use that in the function. So now we have some optional options. So those options that are optional go in brackets, then we have a mandatory argument, so we're not gonna put that in brackets. But optionally we have additional users that we can allow them to specify and they can specify multiple ones, so we'll do this, we'll use an ellipsis there. Now we're going to exit and that will wrap up our usage function. Okay, let's jump to the bottom of our script. And now after we process our options, let's just shift all those options out of the way and then anything that's left over are going to be user accounts and then later we'll loop through those user accounts. So we wanna make sure that the user gives us at least one account to operate against. And if they don't, then that means they don't know what they're doing, so we're gonna give them some help. Call our function that we wrote and close our 'if' statement. Now we're ready to loop through all the usernames that were supplied as arguments. I like to tell whoever's executing the script what user we're processing or working on. Again, they may supply multiple users on the command line. We wanna make sure that we're not removing any system accounts, so we wanna to get the UID of the user that we're processing. Id -u will return the UID, the number for a username. And then we can test against that. So if the UID is less than 1000 then we're not going to remove that account. Before we do anything like delete a home directory, let's make sure we create an archive of it if the user requested us to do that. So if someone passed in the -a option then archive gets set to true, and if archive is true, well, then let's make an archive. Actually, I'm going to create a variable called ARCHIVE_DIR, which represents the archive directory. And I'm gonna use this multiple times in the script. So I think what I'm gonna do is put it at the very top of the script even before our first function here of usage. And really what this is acting as is a constant variable. You could even do this, you could even make it read only if you like. So I'm gonna put it at the top because this is the only thing that I can think of that we might want to adjust later on down the road, for example, if we wanna put these archives on another place in the file system, then we can just change this variable at the very top of our script, make that one small change and not change anything else and be good to go. Okay, let's jump back to the bottom and continue scripting. So what this if statement says is, if not exist directory archive directory then. Okay, so how did I get that exclamation point? Well, you know, that the things in double brackets here are tests and you know that we can get information on the test, we can run by using help test at the command line. And if we look at that, actually I'll just do that now, help test, and it gives us a list of tests, and you can see here that exclamation mark expression means true, f suppression is false, so you can think of that as negating the expression or you can think of the exclamation point as not. So that's where I got that. So ultimately what is saying is if the archive directory doesn't exist, well, guess what? We need to create it. By the way, I'm using the -p option to the mkdir command because if we have archive directory that has multiple sub-directories, for example, we could do something like archive users or whatever. And if the first directory, the parent directory or parent directories don't exist then we need to create them. And that's what that -p option does to the mkdir command, it creates parents if you will. So in our particular case we're just using /archive so it doesn't matter. But if we were to change that variable at the beginning of our script to something that does include sub-directories then we'll need to use a -p. So we're kinda future proofing our script if you will here. So if the mkdir command fails, then that's gonna mean that we can't put any of the archives you create in that directory because the directory didn't exist or wasn't able to be created. So we're gonna bail on our script here. So let's do another check. So we'll check the return status of mkdir. If it's anything, but zero, we have a problem. So now that we know the archive directory exists, let's go ahead and archive the user's home directory and move it into that directory. Now by default, normal user accounts have home directories that live in /home. So if we have adjacent account, then my home directory is gonna be /home/Jason. Now, if you have an application account or a system account, that's probably gonna live somewhere else in the file system. For example, the root home directory is /root and application directory might be /va/www for a web user or something like that. So here we're also using this convention to be another safeguard. If an account doesn't have a home directory in /home then it's probably a system or an application account, and at that point we probably want a system administrator to look at it manually, do it or whatever instead of just whoever happens to be managing accounts. Now, those people I'm sure are knowledgeable about managing accounts. They just may not work on Unix and Linux systems all day like we do. So again, we're just gonna reserve that for a higher level of person that's not using this script. On the other hand, we could have made the decision that we're just going to extract the proper home directory and delete it no matter where it lives on desk or create an archive of it no matter where it lives on disk. So, I can leave that up to you but here I'm just gonna do it this way. So we're going to create an archive file and we're just gonna give it a full path here. We're gonna put it in the archive directory and then we're gonna name it, username, and we're going to make it a compressed tar file. So that's gonna be t-g-z, easy for me to say. And here we go. Here's our check about the home directory. So if the home directory does exist, and that's a good sign, we'll go ahead and move ahead and make our archive. We're gonna compress it, so that's a -z option, c to create an archive, f is the location of the archive file. Then the path to archive, which is the home directory, and tar is gonna make some noise. So we're just gonna send that to dev/null, we don't want the end user to see any output created by tar, just gonna be distracting to them. But we do wanna make sure that tar execute successfully. So if tar exits with a non-zero exit status, we'll say we couldn't create the archive and then exit with a non-zero exit status ourselves. By the way, I just noticed a typing mistake I have up here, I'm used to doing closing bracket and a quotation mark, but I don't need that there. And if I remove it, then I see my syntax highlighting go back to normal here. And so I noticed that because this syntax highlighting looked different. That's what cued me in to that there. So that's kinda good to have an editor with syntax highlighting. Okay, so this, if statement says, hey, if there is a home directory, -d, if this thing called home dir is a directory and exists, then you archive it. And if that doesn't exist or it's not a directory, then we're gonna tell the user, hey, that doesn't exist or it's not what you think it is, we're gonna get out of here, exit one. Now this concludes our if archive equals true if statement. And now we want to see if we need to delete the user or not. So let's do this. So here's what I was talking about earlier with this remove option. So if we're going to delete a user we need to use the userdel command. Additionally, if we want to delete the user's home directory we need the -r option. So if they give us a -r option, we store that and remove underscore option. Now here, remove underscore option we'll evaluate to -r, if they supply -r or it will evaluate to an empty string, in that case it just gonna be like userdel space space username, which is going to give us the exact result that we want. So that is one way to handle this kind of situation. There are other ways but this is a pretty simple way to do this. So we wanna check the exit status of the userdel command because we don't wanna the user thinking that an account got deleted when it really didn't get deleted. Now, I wanna point out something here. We're doing a lot of the same thing here, which is checking for an exit status, if it's not zero then we're gonna give an error message and exit. So I'm doing each one explicitly and changing the echo statement for each one so that the user knows exactly what failed in the script. In theory, you could simplify this a bit and say write a function that will execute a command and then check the return status on that command and then bail or exit your script if it doesn't succeed. So there is some food for thought or maybe that's some extra credit after you write the script this way then maybe write a script and have it use a function called run command or run and check or something like that. Just something to keep in mind. If we make it past the if statement, that means the account was deleted. And by the way, here's another thing, you could have put this inside of the if statement like this, you could have said, if there was a non-zero exit status, we have a problem get out of here. Or if there was not zero, if the exit status was zero more or less, then the account was deleted. I'll leave that up to you, I'm just gonna do it this way. If it makes it pass that if statement then the account was deleted. That makes sense in my mind. So if the delete user is set to true, we do all that above, we use userdel. If it's not set to true then we're just going to disable the account or expire it. We can do that with a chage command. And again, just going to actually duplicate this bit of code, so I'm just gonna copy it and paste it and just change it here. It says the account username was not disabled. Again, if you're copying and pasting, that might be a sign you need to function. Go ahead and grab these two lines as well. Again, if we get past this if statement, that means the account was disabled. Okay, that brings us to the end of that if statement, that also brings us to the end of our four loop and that also brings us to the very end of our script. We're going to exit with a zero exit status. Now I know with long scripts like this I can make some mistakes. And also one of the most common mistakes are typing mistakes, not necessarily logic mistakes. So what I'm gonna do is actually go back to the very beginning of this script and look for things like quotation marks, spelling mistakes, brackets, braces, kind of some normal hot spots that I may have some errors in. And so I'm just gonna jump back to the top and just kinda read things through and correct them as a see them. Okay, the first here I see the script, we can call it this script. That's a minor one in a comment, not gonna make a difference to functionality, but there's a little typing mistake. And there's another one. Okay, here's another one. I have a dollar sign in the wrong place, it should go inside the quotes there. As you can see, can you see the syntax highlighting change? So that's good. That's another error that I found here. Okay, at least I found a couple and corrected them before I executed my script. If not, then I would have just started debugging then, maybe the error message would have given me an indication of where at in the script I had the error and so on. I would have also perhaps gone to the very top here and use some options like -X, -e or -v, or even all three combined there to do some debugging that way or even some manual debugging like this. I would just echo archive_dir and then exit, for example, to create a manual stop point or checkpoint if you will. So those are just a couple of debugging tips. Now that we have our script created before we execute it we need to give it executable permissions. Now let's execute this script without super user privileges and see what happens. It says, please run with sudo or as root. That's what we want. Let's make sure it exited with a non-zero exit status. And sure enough, we have an exit status of one. So this time let's run it with a proper privileges but don't supply any options or don't supply any usernames. Okay, we get a usage statement just like we wanted. And we also get a non-zero exit status which is exactly what we expect. Now let's apply an invalid option. Let's apply a -z, which we don't do. And it says disable local user illegal option -z. Now that is coming from the getups shell built in, but the usage statement of course is coming from our usage of function. Since we know that that function ends with exit one, we can confirm that indeed our script exited with a one exit status. Now let's try to disable a system account. So on these particular systems, we're not really using mail. And I know mail is a system account user with a low UID. So let's go ahead and delete that. Now, if we deleted the mail account then probably nothing bad is gonna happen. And again, these are test systems so we could throw them away and build a new one. So let's try it here. Sudo disable local user and we'll try to disable the mail user. Okay, it says I'm refusing to remove the mail account that has a UID of eight. So that is exactly what we want. Again, a non-zero exit status to go with that. So now what I'm gonna do is actually create some test accounts here to test my script with and actually wrote a script to create these test accounts. I'll just show it to you really quick here. It's a pretty simple script. It just is mainly a four-loop that sets everybody's password to Pass123, creates the account and then sets the password. So I'll just go ahead and add those test accounts now. And now we have a few accounts to play with. Now let's run our script against one of these tests users that we created. Okay, it says the account was disabled, so let's make sure it's disabled by trying to log into it. We'll use our handy password, a pass123 and it says your account has expired, please contact your system administrator. So that's the exact kind of behavior we want. Just to be thorough, let's also make sure that the home directory for that user still exists. Okay, sure enough, it does. Now let's delete the markh account. So, we'll give our script here, we need the -d for the delete option and then the account of markh. Okay, it says processing user and the account was deleted. Let's see if it was. Okay, no such user, that means it's gone. Okay, the home directory still exists, which is the behavior we wanted and expected. Now let's delete a user and their associated home directory. So we can don -dr. Okay, it says the user was deleted. That's right. Okay, home directory is gone. Okay, now let's test the functionality that we have built into our script of operating on multiple users that are supplied as arguments to the script and also let's test the ability to create archives of these user accounts. So let's do this, we'll use -d for delete, r to remove the home directory and a to archive it before it gets removed. And we'll give them these two users. Okay, it says processing user alecg, creating directory/archive. Now that is the directory that we're going to store, this archive files in. And again, that doesn't come by default on a Linux system, so we had to create it ourself in our script. Then it says it's archiving the home alecg directory to archivealecg.tgz. And then it says the account was deleted. And then it moves on to the next account Peterm archive and delete as well. So let's make sure the accounts and home directories are gone. Okay, the account is gone. Account for peterm has also gone, just check these both at once here. Okay, no home directories, but let's make sure the archives were in fact created. Okay, there we have it, 2pgz files one for each count. Let's look at the contents of those archives. The will need a -z, t to list the contents, v will be verbose, f allows us to specify the file. Let's do this one first, that looks good. Okay, and we have the archive of both of those accounts. So that wraps it up for this particular exercise.
Jason is the founder of the Linux Training Academy as well as the author of "Linux for Beginners" and "Command Line Kung Fu." He has over 20 years of professional Linux experience, having worked for industry leaders such as Hewlett-Packard, Xerox, UPS, FireEye, and Amazon.com. Nothing gives him more satisfaction than knowing he has helped thousands of IT professionals level up their careers through his many books and courses.