Linux Shell高级技巧(五)
二十五、通过FTP下载指定的文件:
相比于手工调用FTP命令下载文件,该脚本提供了更为方便的操作方式。
/> cat > test25.sh
#!/bin/sh
#1. 测试脚本参数数量的有效性。
if [ $# -ne 2 ]; then
echo "Usage: $0 ftp://... username" >&2
exit 1
fi
#2. 获取第一个参数的前六个字符,如果不是"ftp://",则视为非法FTP URL格式。这里cut的-c选项表示按照字符的方式截取第一到第六个字符。
header=`echo $1 | cut -c1-6`
if [ "$header" != "ftp://" ]; then
echo "$0: Invalid ftp URL." >&2
exit 1
fi
#3. 合法ftp URL的例子:ftp://ftp.myserver.com/download/test.tar
#4. 针对上面的URL示例,cut命令通过/字符作为分隔符,这样第三个域字段表示server(ftp.myserver.com)。
#5. 在截取filename时,cut命令也是通过/字符作为分隔符,但是"-f4-"将获取从第四个字段开始的后面所有字段(download/test.tar)。
#6. 通过basename命令获取filename的文件名部分。
server=`echo $1 | cut -d/ -f3`
filename=`echo $1 | cut -d/ -f4-`
basefile=`basename $filename`
ftpuser=$2
#7. 这里需要调用stty -echo,以便后面的密码输入不会显示,在输入密码之后,需要再重新打开该选项,以保证后面的输入可以恢复显示。
#8. echo "",是模拟一次换换。
echo -n "Password for $ftpuser: "
stty -echo
read password
stty echo
echo ""
#9. 通过HERE文档,批量执行ftp命令。
echo ${0}: Downloading $baseile from server $server.
ftp -n << EOF
open $server
user $ftpuser $password
get $filename $basefile
quit
EOF
#10.Shell内置变量$?表示上一个Shell进程的退出值,0表示成功执行,其余值均表示不同原因的失败。
if [ $? -eq 0 ]; then
ls -l $basefile
fi
exit 0
CTRL+D
/> ./test25.sh ftp://ftp.myserver.com/download/test.tar stephen
Password for stephen:
./test25.sh: Downloading from server ftp.myserver.com.
-rwxr-xr-x. 1 root root 678 Dec 9 11:46 test.tar
二十六、文件锁定:
在工业应用中,有些来自于工业设备的文件将会被放到指定的目录下,由于这些文件需要再被重新格式化后才能被更高层的软件进行处理。而此时负责处理的脚本程序极有可能是多个实例同时运行,因此这些实例之间就需要一定的同步,以避免多个实例同时操作一个文件而造成的数据不匹配等问题的发生。文件锁定命令可以帮助我们实现这一同步逻辑。
/> cat > test26.sh
#!/bin/sh
#1. 这里需要先确认flock命令是否存在。
if [ -z $(which flock) ]; then
echo "flock doesn't exist."
exit 1
fi
#2. flock中的-e选项表示对该文件加排它锁,-w选项表示如果此时文件正在被加锁,当前的flock命令将等待20秒,如果能锁住该文件,就继续执行,否则退出该命令。
#3. 这里锁定的文件是/var/lock/lockfile1,-c选项表示,如果成功锁定,则指定其后用双引号括起的命令,如果是多个命令,可以用分号分隔。
#4. 可以在两个终端同时启动该脚本,然后观察脚本的输出,以及lockfile1文件的内容。
flock -e -w 20 /var/lock/lockfile1 -c "sleep 10;echo `date` | cat >> /var/lock/lockfile1"
if [ $? -ne 0 ]; then
echo "Fail."
exit 1
else
echo "Success."
exit 0
fi
CTRL+D
二十七、用小文件覆盖整个磁盘:
假设我们现在遇到这样一个问题,公司的关键资料copy到测试服务器上了,在直接将其删除后,仍然担心服务器供应商可以将其恢复,即便是通过fdisk进行重新格式化,也仍然存在被恢复的风险,鉴于此,我们需要编写一个脚本,创建很多小文件(5MB左右),之后不停在关键资料所在的磁盘中复制该文件,以使Linux的inode无法再被重新恢复,为了达到这里效果,我们需要先构造该文件,如:
/> find . -name "*" > testfile
/> ls -l testfile
-rwxr-xr-x. 1 root root 5123678 Dec 9 11:46 testfile
/> cat > test27.sh
#!/bin/sh
#1. 初始化计数器变量,其中max的值是根据当前需要填充的磁盘空间和testfile的大小计算出来的。
counter=0
max=2000000
remainder=0
#2. 每次迭代counter变量都自增一,以保证每次生成不同的文件。当该值大于最大值时退出。
#3. 对计数器变量counter按1000取模,这样可以在每生成1000个文件时打印一次输出,以便看到覆盖的进度,输出时间则便于预估还需要多少时间可以完成。
#4. 创建单独的、用于存放这些覆盖文件的目录。
#5. 生成临时文件,如果写入失败打印出提示信息。
while true
do
((counter=counter+1))
if [ #counter -ge $max ]; then
break
fi
((remainder=counter%1000))
if [ $remainder -eq 0 ]; then
echo -e "counter = $counter\t date = " $(date)
fi
mkdir -p /home/temp2
cat < testfile > "/home/temp/myfiles.$counter"
if [[ $? -ne 0 ]]; then
echo "Failed to wrtie file."
exit 1
fi
done
echo "Done"
CTRL+D
/> ./test27.sh
counter = 1000 Fri Dec 9 17:25:04 CST 2011
counter = 2000 Fri Dec 9 17:25:24 CST 2011
counter = 3000 Fri Dec 9 17:25:54 CST 2011
... ...
与此同时,可以通过执行下面的命令监控磁盘空间的使用率。
/> watch -n 2 'df -h'
Every 2.0s: df -h Fri Dec 9 17:31:56 2011
Filesystem Size Used Avail Use% Mounted on
/dev/sda2 3.9G 2.3G 1.4G 63% /
tmpfs 504M 100K 504M 1% /dev/shm
/dev/sda1 49M 36M 11M 77% /boot
/dev/sda3 15G 172M 14G 2% /home
我们也可以在执行的过程中通过pidstat命令监控脚本进程的每秒读写块数。
二十八、统计当前系统中不同运行状态的进程数量:
在Linux系统中,进程的运行状态主要分为四种:运行时、睡眠、停止和僵尸。下面的脚本将统计当前系统中,各种运行状态的进程数量。
/> cat > test28.sh
#!/bin/sh
#1. 初始化计数器变量,分别对应于运行时、睡眠、停止和僵尸。
running=0
sleeping=0
stopped=0
zombie=0
#2. 在/proc目录下,包含很多以数字作为目录名的子目录,其含义为,每个数字对应于一个当前正在运行进程的pid,该子目录下包含一些文件用于描述与该pid进程相关的信息。如1表示init进程的pid。那么其子目录下的stat文件将包含和该进程运行状态相关的信息。
#3. cat /proc/1/stat,通过该方式可以查看init进程的运行状态,同时也可以了解该文件的格式,其中第三个字段为进程的运行状态字段。
#4. 通过let表达式累加各个计数器。
for pid in /proc/[1-9]*
do
((procs=procs+1))
stat=`awk '{print $3}' $pid/stat`
case $stat in
R) ((running=runing+1));;
S) ((sleeping=sleeping+1));;
T) ((stopped=stopped+1));;
Z) ((zombie=zombie+1));
esac
done
echo -n "Process Count: "
echo -e "Running = $running\tSleeping = $sleeping\tStopped = $stopped\tZombie = $zombie."
CTRL+D
/> ./test28.sh
Process Count: Running = 0 Sleeping = 136 Stopped = 0 Zombie = 0.
二十九、浮点数验证:
浮点数数的重要特征就是只是包含数字0到9、负号(-)和点(.),其中负号只能出现在最前面,点(.)只能出现一次。
/> cat > test29.sh
#!/bin/sh
#1. 之前的一个条目已经介绍了awk中match函数的功能,如果匹配返回匹配的位置值,否则返回0。
#2. 对于Shell中的函数而言,返回0表示成功,其他值表示失败,该语义等同于Linux中的进程退出值。调用者可以通过内置变量$?获取返回值,或者作为条件表达式的一部分直接判断。
validint() {
ret=`echo $1 | awk '{start = match($1,/^-?[0-9]+$/); if (start == 0) print "1"; else print "0"}'`
return $ret
}
validfloat() {
fvalue="$1"
#3. 判断当前参数中是否包含小数点儿。如果包含则需要将其拆分为整数部分和小数部分,分别进行判断。
if [ ! -z $(echo $fvalue | sed 's/[^.]//g') ]; then
decimalpart=`echo $fvalue | cut -d. -f1`
fractionalpart=`echo $fvalue | cut -d. -f2`
#4. 如果整数部分不为空,但是不是合法的整型,则视为非法格式。
if [ ! -z $decimalpart ]; then
if ! validint "$decimalpart" ; then
echo "decimalpart is not valid integer."
return 1
fi
fi
#5. 判断小数部分的第一个字符是否为-,如果是则非法。
if [ "${fractionalpart:0:1}" = "-" ]; then
echo "Invalid floating-point number: '-' not allowed after decimal point." >&2
return 1
fi
#6. 如果小数部分不为空,同时也不是合法的整型,则视为非法格式。
if [ "$fractionalpart" != "" ]; then
if ! validint "$fractionalpart" ; then
echo "fractionalpart is not valid integer."
return 1
fi
fi
#7. 如果整数部分仅为-,或者为空,如果此时小数部分也是空,则为非法格式。
if [ "$decimalpart" = "-" -o -z "$decimalpart" ]; then
if [ -z $fractionalpart ]; then
echo "Invalid floating-point format." >&2
return 1
fi
fi
else
#8. 如果当前参数仅为-,则视为非法格式。
if [ "$fvalue" = "-" ]; then
echo "Invalid floating-point format." >&2
return 1
fi
#9. 由于参数中没有小数点,如果该值不是合法的整数,则为非法格式。
if ! validint "$fvalue" ; then
echo "Invalid floating-point format." >&2
return 1
fi
fi
return 0
}
if validfloat $1 ; then
echo "$1 is a valid floating-point value."
fi
exit 0
CTRL+D
/> ./test29.sh 47895
47895 is a valid floating-point value.
/> ./test29.sh 47895.33
47895.33 is a valid floating-point value.
/> ./test29.sh 47895.3e
fractionalpart is not valid integer.
/> ./test29.sh 4789t.34
decimalpart is not valid integer.
三十、统计英文文章中每个单词出现的频率:
这个技巧的主要目的是显示如何更好的使用awk命令的脚本。
/> cat > test30.sh
#!/bin/sh
#1. 通过当前脚本的pid,生成awk脚本的临时文件名。
#2. 捕捉信号,在脚本退出时删除该临时文件,以免造成大量的垃圾临时文件。
awk_script_file="/tmp/scf_tmp.$$"
trap "rm -f $awk_script_file" EXIT
#3. while循环将以当前目录下的testfile作为输入并逐行读取,在读取到末尾时退出循环。
#4. getline读取到每一行将作为awk的正常输入。在内层的for循环中,i要从1开始,因为$0表示整行。NF表示域字段的数量。
#5. 使$i作为数组的键,如果$i的值匹配正则表达式"^[a-zA-Z]+$",我们将其视为单词而不是标点。每次遇到单词时,其键值都会递增。
#6. 最后通过awk脚本提供的特殊for循环,遍历数组的键值数据。
cat << 'EOF' > $awk_script_file
BEGIN {
while (getline < "./testfile" > 0) {
for (i = 1; i <= NF; ++i) {
if (match($i,"^[a-zA-Z]+$") != 0)
arr[$i]++
}
}
for (word in arr) {
printf "word = %s\t count = %s\n",word,arr[word]
}
}
EOF
awk -f $awk_script_file
CTRL+D
/> cat testfile
hello world liu liu , ,
stephen liu , ?
/> ./test30.sh
word = hello count = 1
word = world count = 1
word = stephen count = 1
word = liu count = 3