This website requires JavaScript.

Shell 脚本编写技巧

采集终端信息并操作

获取终端的行数和列数

tput cols
tput lines

打印当前的终端名

tput longname

将光标移动到座标(100,100)处

tput cup 100 100

设置终端背景色

tput setb n  # n可以在0到7之间取值。

设置终端前景色

tput setf n # n可以在0到7之间取值。

设置文本样式为粗体

tput bold

设置下划线的起止

tput smul
tput rmul

删除从当前光标位置到行为的所有内容:

tput ed

使用stty实现输入密码时不显示输入内容。

#!/bin/sh
#Filename: password.sh
echo -e "Enter password: "
# 在读取密码前禁止回显
stty -echo
read password
# 重新允许回显
stty echo
echo
echo Password read.

输入密码时,脚本不应该显示输入内容。

时间处理

日期能够以多种格式呈现。在系统内部,日期被存储成一个整数,其取值为自1970年1月1日0时0分0秒起所流逝的秒数。这种计时方式成为纪元时或Unix时间。

打印纪元时

$ date +%s
$ date --date "Wed mar 15 08:09:16 EDT 2017" +%s
date --date "Jan 20 2001" +%A
用带有前缀+的格式化字符串作为date命令的参数,可以按照你的选择打印出相应格式的日期。
$ date "+%d %B %Y"

使用date命令计算一组命令所花费的执行时间

#!/bin/bash
start=$(date +%s)
commands;
statements;
end=$(date +%s)
difference=$(( end - start))
echo Time taken to execute commands is $difference seconds.

对命令计时的另一种更好的方式是使用time命令time commandOrScriptName

计算两个日期之间相隔了多少秒:

secs1=`date -d "Jan 2 1970"
secs2=`date -d "Jan 3 1970"
echo "There are `expr $secs2 - $secs1` seconds between Jan 2 and Jan 3"
There are 86400 seconds between Jan 2 and Jan 3

计时脚本

变量一次使用了由seq命令生成的一系列数字。我们用tput sc存储光标位置。在每次循环中,通过tput rc恢复之前存储的光标位置,在终端中打印出新count值,然后使用tputs ed 清除从当前光标位置到行为之间的所有内容。行被清空之后,脚本就可以显示出新的值。

#!/bin/bash
echo Count :
tput sc
# 循环40秒
for count in `seq 0 40`
do
  tput rc
  tput ed
  echo -n $count
  sleep 1
done

函数和参数

函数定义

function fname()
{
    statements;
}

或者

fname()
{
    statements;
}

或者

fname() { statement; }

调用函数

使用函数名即可调用函数

$ fname ; #执行函数

访问函数参数

fname arg1 arg2 ; #传递参数

以下是函数fname的定义。在函数fname中,包含了各种访问函数参数的方法。

fname()
{
    echo $1, $2; #访问参数1和参数2
    echo "$@"; #以列表的方式一次性打印所有参数
    echo "$*";  #类似于$@,但是所有参数被视为单个实体 
    return 0;
}
  • $0是脚本名称
  • $1是第一个参数
  • $2是第二个参数
  • $n是第n个参数
  • ”$@"被扩展成"$1""$2""$3"等。
  • "$*"被扩展成"$1 $2 $3"

递归函数

F() { echo $1; F hello;sleep 1; }

Fork炸弹

以下函数会一直生成新的进程,最终形成拒绝服务攻击。代码的理解和细节请查看维基百科

:(){ :|:& };:

可以修改配置文件/etc/security/limits.conf中的nproc来限制可生成的最大进程数,进而阻止这种攻击。

hard nproc 100 #将所有用户看生成的进行数限制为100

导出函数

函数也能像环境变量一样用export导出,如此一来,函数的作用域就可以扩展到子进程中:

export -f fname
$> function getIP() { /sbin/ifconfig $1 | grep 'inet '; }
$> echo "getIP eth0" >test.sh
$> sh test.sh
  sh: getIP: No such file or directory
$> export -f getIP
$> sh test.sh
inet addr: 192.168.1.2 Bcast: 192.168.255.255 Mask:255.255.0.0

处理参数

一般处理方式为迭代所有的命令行参数,下面语句中shift命令可以依次向左移动一个位置,让脚本能够使用$1来访问到每一个参数。

    for i in `seq 1 $#`
    do
    echo $i is $1
    shift
    done

从键盘或标准输入中读取文本

从输入中读取n个字符并存如变量

read -n number_of_chars variable_name

用无回显的方式读取密码

read -s var

使用read显示提示信息

read -p "Enter input:" var

在给定实现内读取输入

read -t 2 var #在两秒钟内键入的字符串读入变量var

用特定的定界符作为输入行的结束

read -d ":" var  #输入hello:  变量var会被复制为hello

一些小技巧

获得变量值的长度

length=${#var}

识别当前所使用的shell

echo $SHELL 或 echo $0

检查是否为超级用户

If [ $UID -ne 0 ]; then
    echo Non root user. Please run as root.
else
    echo Root user
fi

[实际上是一个命令,必须将其与剩余的字符串用空格隔开。上面的脚本也可以写成:

If test $UID -ne 0:1
  then
    echo Non root user. Please run as root.
  else
    echo Root user
fi

修改Bash的提示字符串(username@hostname:~$)

修改变量PS1即可,默认在~/.bashrc中的某一行

使用函数添加环境变量

prepend() { [ -d "$2" ] && eval $1=\"$2':'\$$1\" && export $1; }
prepend PATH /opt/myapp/bin
prepend LD_LIBRARY_PATH /opt/myapp/lib

函数prepend()首先确认该函数第二个参数所指定的目录是否存在。如果存在,eval表达式将第一个参数指定的变量设置成第二个参数的值加上:(路径分隔符),随后再跟上第一个参数的原始值。 在进行添加时,如果变量为空,则会在末尾留下一个:。要解决这个问题,可以对该函数再做一些修改:

prepend() { [ -d "$2" ] && eval $1=\"$2\$\{$1:+':'\$$1\}\" && export $1 ; }

在这个函数中,我们引入了一种shell参数扩展的形式:${parmeter:+expression},如果parameter有值且不为空,则使用expression的值。通过这次修改,在向环境变量中添加新路径时,当且仅当旧值存在,才会增加。

创建别名rm,删除原始文件并且在backup目录中保留副本。

alias rm='cp $@ ~/backup && rm $@'

将命令序列的输出赋给变量

cmd_output=$(ls | cat -n)

或者

cmd_output=`ls | cat -n`

通过子shell的方式保留空格和换行副

$ cat text.txt
1
2
3
$ out=$(cat text.txt)
$ echo $out
1 2 3 # 丢失了1、2、3中的\n
$ out="$(cat text.txt)"
$ echo $out
1
2
3

持续运行命令直至执行成功

有时候命令只有在满足某些条件时才能够成功执行。这种情况下,你可能希望重复执行命令。以下函数包含了一个无线while循环,该循环执行以函数参数形式(通过$@访问)传入的命令。如果命令执行成功,则返回,进而退出循环。

repeat()
{
    while true
    do
        $@ && return
    done
}

repeat() { while true; do $@ && return; done }

在大多数系统中true是作为/bin中一个二进制文件实现的。这就意味着每次执行依次之前提到的while循环,shell就不得不生成一个进程。为了避免这种情况,可以使用shell的内建命令:,该命令的退出状态总是为0

repeat() { while :; do $@ && return; done }

字符分隔符与迭代器

当IFS被设置为逗号时,shell将视逗号为一个定界符,因此变量$item在每次迭代中读取由逗号分割的子串作为变量值。

data="name, gender,rollno,location"

oldIFS=$IFS
IFS=, #IFS现在被设置为,
for item in $data;
do
    echo Item: $item
done
IFS=$oldIFS
0条评论
avatar