第18章 脚本范例

18.1 批量添加用户脚本

用户管理是Linux系统维护的工作之一,其中涉及用户添加、删除等简单操作。但是如果需要一次性添加几十个还甚至上百个用户,这时候再简单地重复性使用useradd进行添加就非常低效了。现在来分析一下要完成这个工作的场景和必要的实现方式。

首先,需要一个包含所有需添加用户的用户名和密码的文本文件,该文件以行为单位,每行是一条用户信息,用户名和密码之间使用特定的分隔符分开,可以是空格、Tab键或冒号等(不同的分隔符只是脚本处理时的方法不同,没有本质区别)。为了让编写该脚本的过程遇到更多的问题,这里特意选择使用空格作为分隔符,根据这个方案写出的文本文档如下所示:


  1. [root@localhost ~]# cat addusers.txt username001 password001

  2. username002 password002

  3. username003 password003

  4. username004 password004

  5. username005 password005

  6. username006 password006


现在我们面临的是如何根据这个文件添加用户了。想一想,如果是让你根据这个文本来手工添加用户,你会如何操作?大概首先想到的是从第一行开始操作,第一行的用户名是username001,密码是password001,然后使用useradd username001增加用户,再使用passwd username001给用户设置密码,以此类推,直到完成最后一个用户的添加操作——注意这是一个循环,所以需要使用循环来让系统做这件事情。问题又来了,Shell中的循环有很多种,有for循环、while循环、until循环,应该使用哪一种循环呢?

使用for循环和while循环都可以较方便地按行读取,但是笔者更倾向于使用while循环,因为while循环在按行读取时有着天然的优势。先看一下使用for循环按行读取脚本的情况,以及运行结果。


  1. #for

  2. 循环按行读取脚本

  3. [root@localhost ~]# cat useradd_for01.sh #!/bin/bash

  4. COUNT=0

  5. for LINES in `cat addusers.txt`

  6. do

  7. echo $LINES

  8. let COUNT+=1

  9. done

  10. echo

  11. echo "$0 looped $COUNT times"

  12. #for

  13. 循环按行读取脚本运行结果

  14. [root@localhost ~]# bash useradd_for01.sh username001

  15. password001

  16. username002

  17. password002

  18. username003

  19. password003

  20. username004

  21. password004

  22. username005

  23. password005

  24. username006

  25. password006

  26. useradd_for01.sh looped 12 times


从上面的脚本运行输出可以看出,该脚本实际上并没有做到“按行读取”,因为它实际上循环了12次!不是应该循环6次吗,怎么会是12次呢?这是因为addusers.txt文件中的用户名和密码是使用空格隔开的,而for循环在读取文件时,任何空白字符都可以作为其读取的分隔符,所以它其实循环了12次。

来看看while循环的按行读取脚本的情况,以及运行结果。


  1. #while 循环按行读取脚本

  2. [root@localhost ~]# cat useradd_while01.sh #!/bin/bash

  3. COUNT=0

  4. while read LINES

  5. do

  6. echo $LINES

  7. let COUNT+=1

  8. done < addusers.txt echo

  9. echo "$0 looped $COUNT times"

  10. #while

  11. 循环按行读取脚本运行结果

  12. [root@localhost ~]# bash useradd_while01.sh username001 password001

  13. username002 password002

  14. username003 password003

  15. username004 password004

  16. username005 password005

  17. username006 password006

  18. useradd_while01.sh looped 6 times


从脚本运行结果来看,while的按行读取确实没有问题,因为while使用的是换行符作为行标记。如果一开始我们决定使用其他符号作为分隔符(比如冒号),那for循环也能胜任该项工作,但是这样读者就不会注意到这个问题了。这也是前面笔者特意选择空格作为分隔符的原因。

接下来需要使用字符工具对每行进行加工:从每行中分拆出用户名和密码。每行空格前的部分是用户名,空格后的部分为密码。只需要使用cut命令就能很简单地分拆开来了,把useradd_while01.sh升级成下面的内容,并运行。查看结果,确认脚本正确地截取了需要的信息。


  1. [root@localhost ~]# cat useradd_while02.sh #!/bin/bash

  2. while read LINES

  3. do

  4. USERNAME=`echo $LINES | cut -f1 -d' '`

  5. PASSWORD=`echo $LINES | cut -f2 -d' '`

  6. #

  7. 测试打印截取结果

  8. echo -n "USERNAME:$USERNAME PASSWORD:$PASSWORD"

  9. echo

  10. done < addusers.txt #

  11. 脚本成功截取了用户名和密码

  12. [root@localhost ~]# bash useradd_while02.sh USERNAME:username001 PASSWORD:password001

  13. USERNAME:username002 PASSWORD:password002

  14. USERNAME:username003 PASSWORD:password003

  15. USERNAME:username004 PASSWORD:password004

  16. USERNAME:username005 PASSWORD:password005

  17. USERNAME:username006 PASSWORD:password006


现在既然可以成功地截取到每行的用户名、密码,那么事情就变得简单了:在新增用户时,只需要先使用useradd USERNAME命令、然后使用passwd USERNAME命令就可以了。

不过这里有个问题,还记得passwd命令是怎么运行的吗——它要求管理员手工输入密码。有没有办法能直接将密码当作一个参数传给passwd命令呢?答案是肯定的:运行man passwd。查看该命令的用法可以发现,通过使用—stdin参数,可以使用管道将密码传给passwd命令。


  1. --stdin This option is used to indicate that passwd should read the new password from standard input, which can be a pipe.


至此该脚本的基本框架就分析完了,最后将脚本修改为如下内容,运行并观察结果。


  1. [root@localhost ~]# cat useradd_while03.sh #!/bin/bash

  2. while read LINES

  3. do

  4. USERNAME=`echo $LINES | cut -f1 -d' '`

  5. PASSWORD=`echo $LINES | cut -f2 -d' '`

  6. useradd $USERNAME

  7. echo $PASSWORD | passwd --stdin $USERNAME

  8. done < addusers.txt #

  9. 脚本运行结果

  10. [root@localhost ~]# bash useradd_while03.sh Changing password for user username001.

  11. passwd: all authentication tokens updated successfully.

  12. Changing password for user username002.

  13. passwd: all authentication tokens updated successfully.

  14. Changing password for user username003.

  15. passwd: all authentication tokens updated successfully.

  16. Changing password for user username004.

  17. passwd: all authentication tokens updated successfully.

  18. Changing password for user username005.

  19. passwd: all authentication tokens updated successfully.

  20. Changing password for user username006.

  21. passwd: all authentication tokens updated successfully.


虽然说这个脚本现在已经“可以工作”了,但还不是那么完美。如果你再运行一遍该脚本,会发现新增用户是失败的(因为之前运行过一次后,在运行时用户已经存在),但是却又成功地“修改”了用户的密码,这是很危险的。所以需要在脚本中增加用户判断的环节:如果用户已存在,则跳过不做任何修改,否则增加用户。另外,在可能的情况下,所有非Shell内建命令都建议使用全路径,以避免由于环境变量的问题造成的command not found。最后,脚本主体要尽量少使用常量,所以需要在脚本的开头多定义变量。按上面的要求完成脚本修改,最终形式如下:


  1. [root@localhost ~]# cat useradd_while04.sh #!/bin/bash

  2. USERS_INFO=/root/addusers.txt USERADD=/usr/sbin/useradd PASSWD=/usr/bin/passwd CUT=/bin/cut

  3. while read LINES

  4. do

  5. USERNAME=`echo $LINES | $CUT -f1 -d' '`

  6. PASSWORD=`echo $LINES | $CUT -f2 -d' '`

  7. $USERADD $USERNAME

  8. if [ $? -ne 0 ]; then echo "$USERNAME exists, skip set password"

  9. else

  10. echo $PASSWORD | $PASSWD --stdin $USERNAME

  11. fi

  12. done < $USERS_INFO


读者可以在此基础上增加更多的功能和判断使其变得更为完美。