Linux Shell高级技巧(四)
十九、将文件的输出格式化为指定的宽度:
在这个技巧中,不仅包含了如何获取和文件相关的详细信息,如行数,字符等,而且还可以让文件按照指定的宽度输出。这种应用在输出帮助信息、License相关信息时还是比较有用的。
/> cat > test19.sh
#!/bin/sh
#1. 这里我们将缺省宽度设置为75,如果超过该宽度,将考虑折行显示,否则直接在一行中全部打印输出。这里只是为了演示方便,事实上,你完全可以将该值作为脚本或函数的参数传入,那样你将会得到更高的灵活性。
my_width=75
#2. for循环的读取列表来自于脚本的参数。
#3. 在获取lines和chars变量时,sed命令用于过滤掉多余的空格字符。
#4. 在if的条件判断中${#line}用于获取line变量的字符长度,这是Shell内置的规则。
#5. fmt -w 80命令会将echo输出的整行数据根据其命令选项指定的宽度(80个字符)进行折行显示,再将折行后的数据以多行的形式传递给sed命令。
#6. sed在收到fmt命令的格式化输出后,将会在折行后的第一行头部添加两个空格,在其余行的头部添加一个加号和一个空格以表示差别。
for input; do
lines=`wc -l < $input | sed 's/ //g'`
chars=`wc -c < $input | sed 's/ //g'`
owner=`ls -l $input | awk '{print $3}'`
echo "-------------------------------------------------------------------------------"
echo "File $input ($lines lines, $chars characters, owned by $owner):"
echo "-------------------------------------------------------------------------------"
while read line; do
if [ ${#line} -gt $my_width ]; then
echo "$line" | fmt -w 80 | sed -e '1s/^/ /' -e '2,$s/^/+ /'
else
echo " $line"
fi
done < $input
echo "-------------------------------------------------------------------------------"
done | more
CTRL+D
/> ./test19.sh testfile
-------------------------------------------------------------------------------
File testfile.dat (3 lines, 645 characters, owned by root):
-------------------------------------------------------------------------------
The PostgreSQL Global Development Group today released updates for all
+ active branches of the PostgreSQL object-relational database system,
+ including versions 9.1.2, 9.0.6, 8.4.10, 8.3.17 and 8.2.23. Users of any of
+ the several affected features in this release, including binary replication,
+ should update their PostgreSQL installations as soon as possible.
This is also the last update for PostgreSQL 8.2, which is now End-Of-Life
+ (EOL). Users of version 8.2 should plan to upgrade their PostgreSQL
+ installations to 8.3 or later within the next couple of months. For more
+ information, see our Release Support Policy.
This is just a test file.
-------------------------------------------------------------------------------
二十、监控指定目录下磁盘使用空间过大的用户:
在将Linux用作文件服务器时,所有的注册用户都可以在自己的主目录下存放各种类型和大小的文件。有的时候,有些用户的占用空间可能会明显超过其他人,这时就需要管理员可以及时发现这一异常使用状况,并根据实际情况作出应对处理。
/> cat > test20.sh
#!/bin/sh
#1. 该脚本仅用于演示一种处理技巧,其中很多阈值都是可以通过脚本参来初始化的,如limited_qutoa和dirs等变量。
limited_quota=200
dirs="/home /usr /var"
#2. 以冒号作为分隔符,截取passwd文件的第一和第三字段,然后将输出传递给awk命令。
#3. awk中的$2表示的是uid,其中1-99是系统保留用户,>=100的uid才是我们自己创建的用户,awk通过print输出所有的用户名给for循环。
#4. 注意echo命令的输出是由八个单词构成,同时由于-n选项,echo命令并不输出换行符。
#5. 之所以使用find命令,也是为了考虑以点(DOT)开头的隐藏文件。这里的find将在指定目录列表内,搜索指定用户的,类型为普通文件的文件。并通过-ls选项输出找到文件的详细信息。其中输出的详细信息的第七列为文件大小列。
#6. 通过awk命令累加find输出的第七列,最后再在自己的END块中将sum的值用MB计算并输出。该命令的输出将会与上面echo命令的输出合并作为for循环的输出传递给后面的awk命令。这里需要指出的是,该awk的输出就是后面awk命令的$9,因为echo仅仅输出的8个单词。
#7. 从for循环管道获取数据的awk命令,由于awk命令执行的动作是用双引号括起的,所以表示域字段的变量的前缀$符号,需要用\进行转义。变量$limited_quota变量将会自动完成命令替换,从而构成该awk命令的最终动作参数。
for name in `cut -d: -f1,3 /etc/passwd | awk -F: '$2 > 99 {print $1}'`
do
echo -n "User $name exceeds disk quota. Disk Usage is: "
find $dirs -user $name -type f -ls |\
awk '{ sum += $7 } END { print sum / (1024*1024) " MB" }'
done | awk "\$9 > $limited_quota { print \$0 }"
CTRL+D
/> ./test20.sh
二十一、编写一个更具可读性的df命令输出脚本:
这里我们将以awk脚本的方式来实现df -h的功能。
/> cat > test21.sh
#!/bin/sh
#1. $$表示当前Shell进程的pid。
#2. trap信号捕捉是为了保证在Shell正常或异常退出时,仍然能够将该脚本创建的临时awk脚本文件删除。
awk_script_file="/tmp/scf_tmp.$$"
trap "rm -f $awk_script_file" EXIT
#3. 首先需要说明的是,'EOF'中的单引号非常重要,如果忽略他将无法通过编译,这是因为awk的命令动作必须要用单引号扩住。
#4. awk脚本的show函数中,int(mb * 100) / 100这个技巧是为了保证输出时保留小数点后两位。
cat << 'EOF' > $awk_script_file
function show(size) {
mb = size / 1024;
int_mb = (int(mb * 100)) / 100;
gb = mb / 1024;
int_gb = (int(gb * 100)) / 100;
if (substr(size,1,1) !~ "[0-9]" || substr(size,2,1) !~ "[0-9]") {
return size;
} else if (mb < 1) {
return size "K";
} else if (gb < 1) {
return int_mb "M";
} else {
return int_gb "G";
}
}
#5. 在BEGIN块中打印重定义的输出头信息。
BEGIN {
printf "%-20s %7s %7s %7s %8s %s\n","FileSystem","Size","Used","Avail","Use%","Mounted"
}
#6. !/Filesystem/ 表示过滤掉包含Filesystem的行,即df输出的第一行。其余行中,有个域字段可以直接使用df的输出,有的需要通过show函数的计算,以得到更为可读的显示结果。
!/Filesystem/ {
size = show($2);
used = show($3);
avail = show($4);
printf "%-20s %7s %7s %7s %8s %s\n",$1,size,used,avail,$5,$6
}
EOF
df -k | awk -f $awk_script_file
CTRL+D
/> ./test12.sh
FileSystem Size Used Avail Use% Mounted
/dev/sda2 3.84G 2.28G 1.36G 63% /
tmpfs 503.57M 100K 503.47M 1% /dev/shm
/dev/sda1 48.41M 35.27M 10.63M 77% /boot
/dev/sda3 14.8G 171.47M 13.88G 2% /home
二十二、编写一个用于添加新用户的脚本:
之所以在这里选择这个脚本,没有更多的用意,只是感觉这里的有些技巧和常识还是需要了解的,如/etc/passwd、/etc/shadow、/etc/group的文件格式等。
/> cat > test22.sh
#!/bin/sh
#1. 初始化和用户添加相关的变量。
passwd_file="/etc/passwd"
shadow_file="/etc/shadow"
group_file="/etc/group"
home_root_dir="/home"
#2. 只有root用户可以执行该脚本。
if [ "$(whoami)" != "root" ]; then
echo "Error: You must be roor to run this command." >&2
exit 1
fi
echo "Add new user account to $(hostname)"
echo -n "login: "
read login
#3. 去唯一uid,即当前最大uid值加一。
uid=`awk -F: '{ if (big < $3 && $3 < 5000) big = $3 } END {print big + 1}' $passwd_file`
#4. 设定新用户的主目录变量
home_dir=$home_root_dir/$login
gid=$uid
#5. 提示输入和创建新用户相关的信息,如用户全名和主Shell。
echo -n "full name: "
read fullname
echo -n "shell: "
read shell
#6. 将输入的信息填充到passwd、group和shadow三个关键文件中。
echo "Setting up account $login for $fullname..."
echo ${login}:x:${uid}:${gid}:${fullname}:${home_dir}:$shell >> $passwd_file
echo ${login}:*:11647:0:99999:7::: >> $shadow_file
echo "${login}:x:${gid}:$login" >> $group_file
#7. 创建主目录,同时将新用户的profile模板拷贝到新用户的主目录内。
#8. 设定该主目录的权限,再将其下所有文件的owner和group设置为新用户。
#9. 为新用户设定密码。
mkdir $home_dir
cp -R /etc/skel/.[a-zA-Z]* $home_dir
chmod 755 $home_dir
find $home_dir -print | xargs chown ${login}:${login}
passwd $login
exit 0
CTRL+D
/> ./test22.sh
Add new user account to bogon
login: stephen
full name: Stephen Liu
shell: /bin/shell
Setting up account stephen for Stephen Liu...
Changing password for user stephen.
New password:
Retype new password:
passwd: all authentication tokens updated successfully.
二十三、kill指定用户或指定终端的用户进程:
这是一台运行Oracle数据库的Linux服务器,现在我们需要对Oracle做一些特殊的优化工作,由于完成此项工作需要消耗更多的系统资源,因此我们不得不杀掉一些其他用户正在运行的进程,以便节省出更多的系统资源,让本次优化工作能够尽快完成。
/> cat > test23.sh
#!/bin/sh
user=""
tty=""
#1. 通过读取脚本的命令行选项获取要kill的用户或终端。-t后面的参数表示终端,-u后面的参数表示用户。这两个选项不能同时使用。
#2. case中的代码对脚本选项进行验证,一旦失败则退出脚本。
while getopts u:t: opt; do
case $opt in
u) if [ "$tty" != "" ]; then
echo "-u and -t can not be set at one time."
exit 1
fi
user=$OPTARG
;;
t) if [ "$user" != "" ]; then
echo "-u and -t can not be set at one time."
exit 1
fi
tty=$OPTARG
;;
?) echo "Usage: $0 [-u user|-t tty]" >&2
exit 1
esac
done
#3. 如果当前选择的是基于终端kill,就用$tty来过滤ps命令的输出,否则就用$user来过滤ps命令的输出。
#4. awk命令将仅仅打印出pid字段,之后传递给sed命令,sed命令删除ps命令输出的头信息,仅保留后面的进程pids作为输出,并初始化pids数组。
if [ ! -z "$tty" ]; then
pids=$(ps cu -t $tty | awk "{print \$2}" | sed '1d')
else
pids=$(ps cu -U $user | awk "{print \$2}" | sed '1d')
fi
#5. 判断数组是否为空,空则表示没有符合要求的进程,直接退出脚本。
if [ -z "$pids" ]; then
echo "No processes matches."
exit 1
fi
#6. 遍历pids数组,逐个kill指定的进程。
for pid in $pids; do
echo "Killing process[pid = $pid]... ..."
kill -9 $pid
done
exit 0
CTRL+D
/> ./test23.sh -t pts/1
Killing process[pid = 11875]... ...
Killing process[pid = 11894]... ...
/> ./test23.sh -u stephen
Killing process[pid = 11910]... ...
Killing process[pid = 11923]... ...
二十四、判断用户输入(是/否)的便捷方法:
对于有些交互式程序,经常需要等待用户的输入,以便确定下一步的执行流程。通常而言,当用户输入"Y/y/Yes/yes"时表示确认当前的行为,而当输入"N/n/No/no"时则表示否定当前的行为。基于这种规则,我们可以实现一个便捷确认方式,即只判断输入的首字母,如果为Y或y,表示确认,如为N或n,则为否定。
/> cat > test24.sh
#!/bin/sh
echo -n "Please type[y/n/yes/no]: "
read input
#1. 先转换小写到大写,再通过cut截取第一个字符。
ret=`echo $input | tr '[a-z]' '[A-Z]' | cut -c1`
if [ $ret = "Y" ]; then
echo "Your input is Y."
elif [ $ret = "N" ]; then
echo "Your input is N."
else
echo "Your input is error."
fi
CTRL+D
/> ./test24.sh
Please type[y/n/yes/no]: y
Your input is Y.
/> ./test24.sh
Please type[y/n/yes/no]: n
Your input is N.