shell

shell编程

Shell的作用是解释执行用户的命令,用户输入一条命令,Shell就解释执行一条,这种方式称为交互式(Interactive),Shell还有一种执行命令的方式称为批处理(Batch),用户事先写一个Shell脚本(Script),其中有很多条命令,让Shell一次把这些命令执行完,而不必一条一条地敲命令。Shell脚本和编程语言很相似,也有变量和流程控制语句,但Shell脚本是解释执行的,不需要编译,Shell程序从脚本中一行一行读取并执行这些命令,相当于一个用户把脚本中的命令一行一行敲到Shell提示符下执行。

由于历史原因,UNIX系统上有很多种Shell:

1.sh(Bourne Shell):由Steve Bourne开发,各种UNIX系统都配有sh。

2.csh(C Shell):由Bill Joy开发,随BSD UNIX发布,它的流程控制语句很像C语言,支持很多Bourne Shell所不支持的功能:作业控制,命令历史,命令行编辑。

3.ksh(Korn Shell):由David Korn开发,向后兼容sh的功能,并且添加了csh引入的新功能,是目前很多UNIX系统标准配置的Shell,在这些系统上/bin/sh往往是指向/bin/ksh的符号链接。

4.tcsh(TENEX C Shell):是csh的增强版本,引入了命令补全等功能,在FreeBSD、Mac OS X等系统上替代了csh。

5.bash(Bourne Again Shell):由GNU开发的Shell,主要目标是与POSIX标准保持一致,同时兼顾对sh的兼容,bash从csh和ksh借鉴了很多功能,是各种Linux发行版标准配置的Shell,在Linux系统上/bin/sh往往是指向/bin/bash的符号链接。虽然如此,bash和sh还是有很多不同的,一方面,bash扩展了一些命令和参数,另一方面,bash并不完全和sh兼容,有些行为并不一致,所以bash需要模拟sh的行为:当我们通过sh这个程序名启动bash时,bash可以假装自己是sh,不认扩展的命令,并且行为与sh保持一致。

6.zsh 的命令补全功能非常强大,可以补齐路径,补齐命令,补齐参数等。

vim /etc/passwd
其中最后一列显示了用户对应的shell类型
root:x:0:0:root:/root:/bin/bash
nobody:x:65534:65534:nobody:/nonexistent:/bin/sh
syslog:x:101:103::/home/syslog:/bin/false
itcast:x:1000:1000:itcast,,,:/home/itcast:/bin/bash
ftp:x:115:125:ftp daemon,,,:/srv/ftp:/bin/false

用户在命令行输入命令后,一般情况下Shell会fork并exec该命令,但是Shell的内建命令例外,执行内建命令相当于调用Shell进程中的一个函数,并不创建新的进程。以前学过的cd、alias、umask、exit等命令即是内建命令,凡是用which命令查不到程序文件所在位置的命令都是内建命令,内建命令没有单独的man手册,要在man手册中查看内建命令,应该

$ man bash-builtins

如export、shift、if、eval、[、for、while等等。内建命令虽然不创建新的进程,但也会有Exit Status,通常也用0表示成功非零表示失败,虽然内建命令不创建新的进程,但执行结束后也会有一个状态码,也可以用特殊变量$?读出。

执行脚本

编写一个简单的脚本test.sh:

#! /bin/sh
cd ..
ls

Shell脚本中用#表示注释,相当于C语言的//注释。但如果#位于第一行开头,并且是#!(称为Shebang)则例外,它表示该脚本使用后面指定的解释器/bin/sh解释执行。如果把这个脚本文件加上可执行权限然后执行:

chmod a+x test.sh
./test.sh

Shell会fork一个子进程并调用exec执行./test.sh这个程序,exec系统调用应该把子进程的代码段替换成./test.sh程序的代码段,并从它的_start开始执行。然而test.sh是个文本文件,根本没有代码段和_start函数,怎么办呢?其实exec还有另外一种机制,如果要执行的是一个文本文件,并且第一行用Shebang指定了解释器,则用解释器程序的代码段替换当前进程,并且从解释器的_start开始执行,而这个文本文件被当作命令行参数传给解释器。因此,执行上述脚本相当于执行程序

$ /bin/sh ./test.sh

以这种方式执行不需要test.sh文件具有可执行权限。

如果将命令行下输入的命令用()括号括起来,那么也会fork出一个子Shell执行小括号中的命令,一行中可以输入由分号;隔开的多个命令,比如:

$ (cd ..;ls -l)

和上面两种方法执行Shell脚本的效果是相同的,cd ..命令改变的是子Shell的PWD,而不会影响到交互式Shell。然而命令

$ cd ..;ls -l

则有不同的效果,cd ..命令是直接在交互式Shell下执行的,改变交互式Shell的PWD,然而这种方式相当于这样执行Shell脚本:

$ source ./test.sh

或者

$ . ./test.sh

source或者.命令是Shell的内建命令,这种方式也不会创建子Shell,而是直接在交互式Shell下逐行执行脚本中的命令。

 

小结:

./test.sh         //需要可执行权限,不会改变路径(因为是直接执行,并且创建了子线程)
/bin/sh test.sh   //不需要可执行权限,不会改变路径,(因为是当做参数传递给sh去执行)

source test.sh     //不需要可执行权限,会改变路径,因为是内建命令
. (注意有空格)test.sh  //跟source相同

 

基本语法

变量

按照惯例,Shell变量由全大写字母加下划线组成,有两种类型的Shell变量:

1.环境变量

环境变量可以从父进程传给子进程,因此Shell进程的环境变量可以从当前Shell进程传给fork出来的子进程。用printenv命令可以显示当前Shell进程的环境变量。

2.本地变量

只存在于当前Shell进程,用set命令可以显示当前Shell进程中定义的所有变量(包括本地变量和环境变量)和函数。

环境变量是任何进程都有的概念,而本地变量是Shell特有的概念。在Shell中,环境变量和本地变量的定义和用法相似。在Shell中定义或赋值一个变量:

itcast$ VARNAME=value

注意等号两边都不能有空格,否则会被Shell解释成命令和命令行参数。

一个变量定义后仅存在于当前Shell进程,它是本地变量,用export命令可以把本地变量导出为环境变量,定义和导出环境变量通常可以一步完成:

itcast$ export VARNAME=value

也可以分两步完成:

itcast$ VARNAME=value
itcast$ export VARNAME

用unset命令可以删除已定义的环境变量或本地变量。

itcast$ unset VARNAME

如果一个变量叫做VARNAME,用${VARNAME}可以表示它的值,在不引起歧义的情况下也可以用$VARNAME表示它的值。通过以下例子比较这两种表示法的不同:

itcast$ echo $SHELL

注意,在定义变量时不用$,取变量值时要用$。和C语言不同的是,Shell变量不需要明确定义类型,事实上Shell变量的值都是字符串,比如我们定义VAR=45,其实VAR的值是字符串45而非整数。Shell变量不需要先定义后使用,如果对一个没有定义的变量取值,则值为空字符串。

文件名代换(Globbing):* ? []

这些用于匹配的字符称为通配符(Wildcard),具体如下:

通配符

*   匹配0个或多个任意字符
?   匹配一个任意字符
[若干字符]  匹配方括号中任意一个字符的一次出现

$ ls /dev/ttyS*
$ ls ch0?.doc
$ ls ch0[0-2].doc
$ ls ch[012]   [0-9].doc

注意,Globbing所匹配的文件名是由Shell展开的,也就是说在参数还没传给程序之前已经展开了,比如上述ls ch0[012].doc命令,如果当前目录下有ch00.doc和ch02.doc,则传给ls命令的参数实际上是这两个文件名,而不是一个匹配字符串。

命令代换:`或 $()

由'`'反引号括起来的也是一条命令,Shell先执行该命令,然后将输出结果立刻代换到当前命令行中。例如定义一个变量存放date命令的输出:

itcast$ DATE=`date`
itcast$ echo $DATE

命令代换也可以用$()表示:

itcast$ DATE=$(date)

算术代换:$(())

用于算术计算,$(())中的Shell变量取值将转换成整数,同样含义的$[]等价例如:

itcast$ VAR=45
itcast$ echo $(($VAR+3))
$(())中只能用+-*/和()运算符,并且只能做整数运算。

$[base#n],其中base表示进制,n按照base进制解释,后面再有运算数,按十进制解释。

echo $[2#10+11]
echo $[8#10+11]
echo $[10#10+11]

转义字符\

和C语言类似,\在Shell中被用作转义字符,用于去除紧跟其后的单个字符的特殊意义(回车除外),换句话说,紧跟其后的字符取字面值。例如:

itcast$ echo $SHELL
/bin/bash
itcast$ echo \$SHELL
$SHELL
itcast$ echo \\
\

比如创建一个文件名为“$ $”的文件可以这样:

itcast$ touch \$\ \$

还有一个字符虽然不具有特殊含义,但是要用它做文件名也很麻烦,就是-号。如果要创建一个文件名以-号开头的文件,这样是不行的:

itcast$ touch -hello
touch: invalid option -- h
Try `touch --help' for more information.

即使加上\转义也还是报错:

itcast$ touch \-hello
touch: invalid option -- h
Try `touch --help' for more information.

因为各种UNIX命令都把-号开头的命令行参数当作命令的选项,而不会当作文件名。如果非要处理以-号开头的文件名,可以有两种办法:

itcast$ touch ./-hello

或者

itcast$ touch -- -hello

\还有一种用法,在\后敲回车表示续行,Shell并不会立刻执行命令,而是把光标移到下一行,给出一个续行提示符>,等待用户继续输入,最后把所有的续行接到一起当作一个命令执行。例如:

itcast$ ls \
> -l
(ls -l命令的输出)

单引号

和C语言不一样,Shell脚本中的单引号和双引号一样都是字符串的界定符(双引号下一节介绍),而不是字符的界定符。单引号用于保持引号内所有字符的字面值,即使引号内的\和回车也不例外,但是字符串中不能出现单引号。如果引号没有配对就输入回车,Shell会给出续行提示符,要求用户把引号配上对。例如:

itcast$ echo '$SHELL'
$SHELL
itcast$ echo 'ABC\(回车)
> DE'(再按一次回车结束命令)
ABC\
DE

双引号

被双引号用括住的内容,将被视为单一字串。它防止通配符扩展,但允许变量扩展。这点与单引号的处理方式不同

itcast$ DATE=$(date)
itcast$ echo "$DATE"
itcast$ echo '$DATE'

Shell脚本语法

条件测试:test或[

命令test或[可以测试一个条件是否成立,如果测试结果为真,则该命令的Exit Status为0,如果测试结果为假,则命令的Exit Status为1(注意与C语言的逻辑表示正好相反)。例如测试两个数的大小关系:

itcast@ubuntu:~$ var=2
itcast@ubuntu:~$ test $var -gt 1
itcast@ubuntu:~$ echo $?
0
itcast@ubuntu:~$ test $var -gt 3
itcast@ubuntu:~$ echo $?
1
itcast@ubuntu:~$ [ $var -gt 3 ]
itcast@ubuntu:~$ echo $?
1
itcast@ubuntu:~$

虽然看起来很奇怪,但左方括号[确实是一个命令的名字,传给命令的各参数之间应该用空格隔开,比如,$VAR、-gt、3、]是[命令的四个参数,它们之间必须用空格隔开。命令test或[的参数形式是相同的,只不过test命令不需要]参数。以[命令为例,常见的测试命令如下表所示:

[ -d DIR ]              如果DIR存在并且是一个目录则为真
[ -f FILE ]             如果FILE存在且是一个普通文件则为真
[ -z STRING ]           如果STRING的长度为零则为真
[ -n STRING ]           如果STRING的长度非零则为真
[ STRING1 = STRING2 ]   如果两个字符串相同则为真
[ STRING1 != STRING2 ]  如果字符串不相同则为真
[ ARG1 OP ARG2 ]        ARG1和ARG2应该是整数或者取值为整数的变量,OP是-eq(等于)-ne(不等于)-lt(小于)-le(小于等于)-gt(大于)-ge(大于等于)之中的一个

和C语言类似,测试条件之间还可以做与、或、非逻辑运算:

带与、或、非的测试命令

[ ! EXPR ]          EXPR可以是上表中的任意一种测试条件,!表示逻辑反
[ EXPR1 -a EXPR2 ]  EXPR1和EXPR2可以是上表中的任意一种测试条件,-a表示逻辑与
[ EXPR1 -o EXPR2 ]  EXPR1和EXPR2可以是上表中的任意一种测试条件,-o表示逻辑或

例如:

$ VAR=abc
$ [ -d Desktop -a $VAR = 'abc' ]
$ echo $?
0

注意,如果上例中的$VAR变量事先没有定义,则被Shell展开为空字符串,会造成测试条件的语法错误(展开为[ -d Desktop -a = 'abc' ]),作为一种好的Shell编程习惯,应该总是把变量取值放在双引号之中(展开为[ -d Desktop -a "" = 'abc' ]):

$ unset VAR
$ [ -d Desktop -a $VAR = 'abc' ]
bash: [: too many arguments
$ [ -d Desktop -a "$VAR" = 'abc' ]
$ echo $?
1

if/then/elif/else/fi

和C语言类似,在Shell中用if、then、elif、else、fi这几条命令实现分支控制。这种流程控制语句本质上也是由若干条Shell命令组成的,例如先前讲过的

if [ -f ~/.bashrc ]; then
    . ~/.bashrc
fi

其实是三条命令,if [ -f ~/.bashrc ]是第一条,then . ~/.bashrc是第二条,fi是第三条。如果两条命令写在同一行则需要用;号隔开,一行只写一条命令就不需要写;号了,另外,then后面有换行,但这条命令没写完,Shell会自动续行,把下一行接在then后面当作一条命令处理。和[命令一样,要注意命令和各参数之间必须用空格隔开。if命令的参数组成一条子命令,如果该子命令的Exit Status为0(表示真),则执行then后面的子命令,如果Exit Status非0(表示假),则执行elif、else或者fi后面的子命令。if后面的子命令通常是测试命令,但也可以是其它命令。Shell脚本没有{}括号,所以用fi表示if语句块的结束。见下例:

#! /bin/sh

if [ -f /bin/bash ]
then echo "/bin/bash is a file"
else echo "/bin/bash is NOT a file"
fi
if :; then echo "always true"; fi

:是一个特殊的命令,称为空命令,该命令不做任何事,但Exit Status总是真。此外,也可以执行/bin/true或/bin/false得到真或假的Exit Status。再看一个例子:

    #! /bin/sh

    echo "Is it morning? Please answer yes or no."
    read YES_OR_NO
    if [ "$YES_OR_NO" = "yes" ]; then
      echo "Good morning!"
    elif [ "$YES_OR_NO" = "no" ]; then
      echo "Good afternoon!"
    else
      echo "Sorry, $YES_OR_NO not recognized. Enter yes or no."
      exit 1
    fi
    exit 0

上例中的read命令的作用是等待用户输入一行字符串,将该字符串存到一个Shell变量中。

此外,Shell还提供了&&和||语法,和C语言类似,具有Short-circuit特性,很多Shell脚本喜欢写成这样:

test "$(whoami)" != 'root' && (echo you are using a non-privileged account; exit 1)

&&相当于“if...then...”,而||相当于“if not...then...”。&&和||用于连接两个命令,而上面讲的-a和-o仅用于在测试表达式中连接两个测试条件,要注意它们的区别,例如,

test "$VAR" -gt 1 -a "$VAR" -lt 3

和以下写法是等价的

test "$VAR" -gt 1 && test "$VAR" -lt 3

case/esac

case命令可类比C语言的switch/case语句,esac表示case语句块的结束。C语言的case只能匹配整型或字符型常量表达式,而Shell脚本的case可以匹配字符串和Wildcard,每个匹配分支可以有若干条命令,末尾必须以;;结束,执行时找到第一个匹配的分支并执行相应的命令,然后直接跳到esac之后,不需要像C语言一样用break跳出。

    #! /bin/sh

    echo "Is it morning? Please answer yes or no."
    read YES_OR_NO
    case "$YES_OR_NO" in
    yes|y|Yes|YES)
      echo "Good Morning!";;
    [nN]*)
      echo "Good Afternoon!";;
    *)
      echo "Sorry, $YES_OR_NO not recognized. Enter yes or no."
      exit 1;;
    esac
    exit 0

使用case语句的例子可以在系统服务的脚本目录/etc/init.d中找到。这个目录下的脚本大多具有这种形式(以/etc/init.d/nfs-kernel-server为例):

    case "$1" in
        start)
            ...
        ;;
        stop)
            ...
        ;;
        reload | force-reload)
            ...
        ;;
        restart)
        ...
        *)
            log_success_msg "Usage: nfs-kernel-server {start|stop|status|reload|force-reload|restart}"
            exit 1
        ;;
    esac

启动nfs-kernel-server服务的命令是

$ sudo /etc/init.d/nfs-kernel-server start

$1是一个特殊变量,在执行脚本时自动取值为第一个命令行参数,也就是start,所以进入start)分支执行相关的命令。同理,命令行参数指定为stop、reload或restart可以进入其它分支执行停止服务、重新加载配置文件或重新启动服务的相关命令。

for/do/done

Shell脚本的for循环结构和C语言很不一样,它类似于某些编程语言的foreach循环。例如:

    #! /bin/sh

    for FRUIT in apple banana pear; do
      echo "I like $FRUIT"
    done
FRUIT是一个循环变量,第一次循环$FRUIT的取值是apple,第二次取值是banana,第三次取值是pear。再比如,要将当前目录下的chap0、chap1、chap2等文件名改为chap0~、chap1~、chap2~等(按惯例,末尾有~字符的文件名表示临时文件),这个命令可以这样写:

$ for FILENAME in chap?; do mv $FILENAME $FILENAME~; done

也可以这样写:

$ for FILENAME in `ls chap?`; do mv $FILENAME $FILENAME~; done

while/do/done

while的用法和C语言类似。比如一个验证密码的脚本:

    #! /bin/sh

    echo "Enter password:"
    read TRY
    while [ "$TRY" != "secret" ]; do
      echo "Sorry, try again"
      read TRY
    done

下面的例子通过算术运算控制循环的次数:

    #! /bin/sh

    COUNTER=1
    while [ "$COUNTER" -lt 10 ]; do
      echo "Here we go again"
      COUNTER=$(($COUNTER+1))
    done

Shell还有until循环,类似C语言的do...while循环。本章从略。

break和continue

break[n]可以指定跳出几层循环,continue跳过本次循环步,没跳出整个循环。

break跳出,continue跳过。

习题

1、把上面验证密码的程序修改一下,如果用户输错五次密码就报错退出。

位置参数和特殊变量

有很多特殊变量是被Shell自动赋值的,我们已经遇到了$?和$1,现在总结一下:

常用的位置参数和特殊变量

$0  相当于C语言main函数的argv[0]
$1、$2...    这些称为位置参数(Positional Parameter),相当于C语言main函数的argv[1]、argv[2]...
$#  相当于C语言main函数的argc - 1,注意这里的#后面不表示注释
$@  表示参数列表"$1" "$2" ...,例如可以用在for循环中的in后面。
$*  表示参数列表"$1" "$2" ...,同上
$?  上一条命令的Exit Status
$$  当前进程号

位置参数可以用shift命令左移。比如shift 3表示原来的$4现在变成$1,原来的$5现在变成$2等等,原来的$1、$2、$3丢弃,$0不移动。不带参数的shift命令相当于shift 1。例如:

    #! /bin/sh

    echo "The program $0 is now running"
    echo "The first parameter is $1"
    echo "The second parameter is $2"
    echo "The parameter list is $@"
    shift
    echo "The first parameter is $1"
    echo "The second parameter is $2"
    echo "The parameter list is $@"

shell输入输出

echo

echo显示文本行或变量,或者把字符串输入到文件。

echo [option] string
-e 解析转义字符
-n 不回车换行。默认情况echo回显的内容后面跟一个回车换行。
echo "hello\n\n"
echo -e "hello\n\n"
echo  "hello"
echo -n "hello"

管道|

可以通过管道把一个命令的输出传递给另一个命令做输入。管道用竖线表示。

cat myfile | more
ls -l | grep "myfile"
df -k | awk '{print $1}' | grep -v "文件系统"
df -k 查看磁盘空间,找到第一列,去除“文件系统”,并输出

tee

tee命令把结果输出到标准输出,另一个副本输出到相应文件。

df -k | awk '{print $1}' | grep -v "文件系统" | tee a.txt

tee -a a.txt表示追加操作。
df -k | awk '{print $1}' | grep -v "文件系统" | tee -a a.txt

文件重定向

cmd > file             把标准输出重定向到新文件中
cmd >> file            追加
cmd > file 2>&1        标准出错也重定向到1所指向的file里
cmd >> file 2>&1
cmd < file1 > file2    输入输出都定向到文件里
cmd < &fd              把文件描述符fd作为标准输入
cmd > &fd              把文件描述符fd作为标准输出
cmd < &-               关闭标准输入

函数

和C语言类似,Shell中也有函数的概念,但是函数定义中没有返回值也没有参数列表。例如:

    #! /bin/sh

    foo(){ echo "Function foo is called";}
    echo "-=start=-"
    foo
    echo "-=end=-"

注意函数体的左花括号'{'和后面的命令之间必须有空格或换行,如果将最后一条命令和右花括号'}'写在同一行,命令末尾必须有;号。

在定义foo()函数时并不执行函数体中的命令,就像定义变量一样,只是给foo这个名字一个定义,到后面调用foo函数的时候(注意Shell中的函数调用不写括号)才执行函数体中的命令。Shell脚本中的函数必须先定义后调用,一般把函数定义都写在脚本的前面,把函数调用和其它命令写在脚本的最后(类似C语言中的main函数,这才是整个脚本实际开始执行命令的地方)。

Shell函数没有参数列表并不表示不能传参数,事实上,函数就像是迷你脚本,调用函数时可以传任意个参数,在函数内同样是用$0、$1、$2等变量来提取参数,函数中的位置参数相当于函数的局部变量,改变这些变量并不会影响函数外面的$0、$1、$2等变量。函数中可以用return命令返回,如果return后面跟一个数字则表示函数的Exit Status。

下面这个脚本可以一次创建多个目录,各目录名通过命令行参数传入,脚本逐个测试各目录是否存在,如果目录不存在,首先打印信息然后试着创建该目录。

    #! /bin/sh

    is_directory()
    {
      DIR_NAME=$1
      if [ ! -d $DIR_NAME ]; then
        return 1
      else
        return 0
      fi
    }

    for DIR in "$@"; do
      if is_directory "$DIR"
      then :
      else
        echo "$DIR doesn't exist. Creating it now..."
        mkdir $DIR > /dev/null 2>&1
        if [ $? -ne 0 ]; then
          echo "Cannot create directory $DIR"
          exit 1
        fi
      fi
    done

注意is_directory()返回0表示真返回1表示假。

Shell脚本的调试方法

Shell提供了一些用于调试脚本的选项,如下所示:

-n

读一遍脚本中的命令但不执行,用于检查脚本中的语法错误

-v

一边执行脚本,一边将执行过的脚本命令打印到标准错误输出

-x

提供跟踪执行信息,将执行的每一条命令和结果依次打印出来

使用这些选项有三种方法,一是在命令行提供参数

    $ sh -x ./script.sh

二是在脚本开头提供参数

    #! /bin/sh -x

第三种方法是在脚本中用set命令启用或禁用参数

    #! /bin/sh
    if [ -z "$1" ]; then
      set -x
      echo "ERROR: Insufficient Args."
      exit 1
      set +x
    fi

set -x和set +x分别表示启用和禁用-x参数,这样可以只对脚本中的某一段进行跟踪调试。

正则表达式

以前我们用grep在一个文件中找出包含某些字符串的行,比如在头文件中找出一个宏定义。其实grep还可以找出符合某个模式(Pattern)的一类字符串。例如找出所有符合xxxxx@xxxx.xxx模式的字符串(也就是email地址),要求x字符可以是字母、数字、下划线、小数点或减号,email地址的每一部分可以有一个或多个x字符,例如abc.d@ef.com、1_2@987-6.54,当然符合这个模式的不全是合法的email地址,但至少可以做一次初步筛选,筛掉a.b、c@d等肯定不是email地址的字符串。再比如,找出所有符合yyy.yyy.yyy.yyy模式的字符串(也就是IP地址),要求y是0-9的数字,IP地址的每一部分可以有1-3个y字符。

如果要用grep查找一个模式,如何表示这个模式,这一类字符串,而不是一个特定的字符串呢?从这两个简单的例子可以看出,要表示一个模式至少应该包含以下信息:

字符类(Character Class):如上例的x和y,它们在模式中表示一个字符,但是取值范围是一类字符中的任意一个。

数量限定符(Quantifier): 邮件地址的每一部分可以有一个或多个x字符,IP地址的每一部分可以有1-3个y字符

各种字符类以及普通字符之间的位置关系:例如邮件地址分三部分,用普通字符@和.隔开,IP地址分四部分,用.隔开,每一部分都可以用字符类和数量限定符描述。为了表示位置关系,还有位置限定符(Anchor)的概念,将在下面介绍。

规定一些特殊语法表示字符类、数量限定符和位置关系,然后用这些特殊语法和普通字符一起表示一个模式,这就是正则表达式(Regular Expression)。例如email地址的正则表达式可以写成[a-zA-Z0-9_.-]+@[a-zA-Z0-9_.-]+.[a-zA-Z0-9_.-]+,IP地址的正则表达式可以写成[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}。下一节介绍正则表达式的语法,我们先看看正则表达式在grep中怎么用。例如有这样一个文本文件testfile:

192.168.1.1
1234.234.04.5678
123.4234.045.678
abcde

查找其中包含IP地址的行:

$ egrep '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' testfile
192.168.1.1
1234.234.04.5678

egrep相当于grep -E,表示采用Extended正则表达式语法。grep的正则表达式有Basic和Extended两种规范,它们之间的区别下一节再解释。另外还有fgrep命令,相当于grep -F,表示只搜索固定字符串而不搜索正则表达式模式,不会按正则表达式的语法解释后面的参数。

注意正则表达式参数用单引号括起来了,因为正则表达式中用到的很多特殊字符在Shell中也有特殊含义(例如\),只有用单引号括起来才能保证这些字符原封不动地传给grep命令,而不会被Shell解释掉。

192.168.1.1符合上述模式,由三个.隔开的四段组成,每段都是1到3个数字,所以这一行被找出来了,可为什么1234.234.04.5678也被找出来了呢?因为grep找的是包含某一模式的行,这一行包含一个符合模式的字符串234.234.04.567。相反,123.4234.045.678这一行不包含符合模式的字符串,所以不会被找出来。

grep是一种查找过滤工具,正则表达式在grep中用来查找符合模式的字符串。其实正则表达式还有一个重要的应用是验证用户输入是否合法,例如用户通过网页表单提交自己的email地址,就需要用程序验证一下是不是合法的email地址,这个工作可以在网页的Javascript中做,也可以在网站后台的程序中做,例如PHP、Perl、Python、Ruby、Java或C,所有这些语言都支持正则表达式,可以说,目前不支持正则表达式的编程语言实在很少见。除了编程语言之外,很多UNIX命令和工具也都支持正则表达式,例如grep、vi、sed、awk、emacs等等。“正则表达式”就像“变量”一样,它是一个广泛的概念,而不是某一种工具或编程语言的特性。

基本语法

我们知道C的变量和Shell脚本变量的定义和使用方法很不相同,表达能力也不相同,C的变量有各种类型,而Shell脚本变量都是字符串。同样道理,各种工具和编程语言所使用的正则表达式规范的语法并不相同,表达能力也各不相同,有的正则表达式规范引入很多扩展,能表达更复杂的模式,但各种正则表达式规范的基本概念都是相通的。本节介绍egrep(1)所使用的正则表达式,它大致上符合POSIX正则表达式规范,详见regex(7)(看这个man page对你的英文绝对是很好的锻炼)。希望读者仿照上一节的例子,一边学习语法,一边用egrep命令做实验。

字符类

字符  含义               举例
.   匹配任意一个字符          abc.可以匹配abcd、abc9等
[]  匹配括号中的任意一个字符  [abc]d可以匹配ad、bd或cd
-   在[]括号内表示字符范围    [0-9a-fA-F]可以匹配一位十六进制数字
^   位于[]括号内的开头,匹配除括号中的字符之外的任意一个字符  [^xy]匹配除xy之外的任一字符,因此[^xy]1可以匹配a1、b1但不匹配x1、y1

[[:xxx:]]   grep工具预定义的一些命名字符类   [[:alpha:]]匹配一个字母,[[:digit:]]匹配一个数字

 

数量限定符

字符    含义                             举例
?   紧跟在它前面的单元应匹配零次或一次    [0-9]?\.[0-9]匹配0.02.3、.5等,由于.在正则表达式中是一个特殊字符,所以需要用\转义一下,取字面值
+   紧跟在它前面的单元应匹配一次或多次    [a-zA-Z0-9_.-]+@[a-zA-Z0-9_.-]+\.[a-zA-Z0-9_.-]+匹配email地址
*   紧跟在它前面的单元应匹配零次或多次    [0-9][0-9]*匹配至少一位数字,等价于[0-9]+,[a-zA-Z_]+[a-zA-Z_0-9]*匹配C语言的标识符
{N} 紧跟在它前面的单元应精确匹配N次       [1-9][0-9]{2}匹配从100到999的整数
{N,}  紧跟在它前面的单元应匹配至少N次     [1-9][0-9]{2,}匹配三位以上(含三位)的整数
{,M}  紧跟在它前面的单元应匹配最多M次     [0-9]{,1}相当于[0-9]?
{N,M} 紧跟在它前面的单元应匹配至少N次,最多M次   [0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}匹配IP地址

 

再次注意grep找的是包含某一模式的行,而不是完全匹配某一模式的行。再举个例子,如果文本文件的内容是

aaabc
aad
efg

查找a*这个模式的结果是三行都被找出来了

$ egrep 'a*' testfile 
aabc
aad
efg

a匹配0个或多个a,而第三行包含0个a,所以也包含了这一模式。单独用a这样的正则表达式做查找没什么意义,一般是把a*作为正则表达式的一部分来用。

位置限定符

    字符  含义                举例
    ^   匹配行首的位置        ^Content匹配位于一行开头的Content
    $   匹配行末的位置        ;$匹配位于一行结尾的;号,^$匹配空行
    \<  匹配单词开头的位置    \<th匹配... this,但不匹配ethernet、tenth
    \>  匹配单词结尾的位置    p\>匹配leap ...,但不匹配parent、sleepy
    \b  匹配单词开头或结尾的位置     \bat\b匹配... at ...,但不匹配cat、atexit、batch
    \B  匹配非单词开头和结尾的位置   \Bat\B匹配battery,但不匹配... attend、hat ...

 

位置限定符可以帮助grep更准确地查找,例如上一节我们用[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}查找IP地址,找到这两行

192.168.1.1
1234.234.04.5678

如果用^[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}$查找,就可以把1234.234.04.5678这一行过滤掉了。

其它特殊字符

字符  含义    举例
\    转义字符,普通字符转义为特殊字符,特殊字符转义为普通字符   普通字符<写成\<表示单词开头的位置,特殊字符.写成\.以及\写成\\就当作普通字符来匹配
()   将正则表达式的一部分括起来组成一个单元,可以对整个单元使用数量限定符    ([0-9]{1,3}\.){3}[0-9]{1,3}匹配IP地址
|    连接两个子表达式,表示或的关系     n(o|either)匹配no或neither

 

以上介绍的是grep正则表达式的Extended规范,Basic规范也有这些语法,只是字符?+{}|()应解释为普通字符,要表示上述特殊含义则需要加\转义。如果用grep而不是egrep,并且不加-E参数,则应该遵照Basic规范来写正则表达式。

 

sed

sed意为流编辑器(Stream Editor),在Shell脚本和Makefile中作为过滤器使用非常普遍,也就是把前一个程序的输出引入sed的输入,经过一系列编辑命令转换为另一种格式输出。sed和vi都源于早期UNIX的ed工具,所以很多sed命令和vi的末行命令是相同的

注意,sed命令不会修改原文件,删除命令只表示某些行不打印输出,而不是从原文件中删去。

 

sed命令行的基本格式为

sed option 'script' file1 file2 ...
sed option -f scriptfile file1 file2 ...

选项含义:

--version            显示sed版本。
--help               显示帮助文档。
-n,--quiet,--silent  静默输出,默认情况下,sed程序在所有的脚本指令执行完毕后,将自动打印模式空间中的内容,这些选项可以屏蔽自动打印。
-e script            允许多个脚本指令被执行。//也就是后面跟着一个文件,而不是一个跟着一行命令
-f script-file, 
--file=script-file   从文件中读取脚本指令,对编写自动脚本程序来说很棒!
-i,--in-place        直接修改源文件,经过脚本指令处理后的内容将被输出至源文件(源文件被修改)慎用!
-l N, --line-length=N 该选项指定l指令可以输出的行长度,l指令用于输出非打印字符。
--posix             禁用GNU sed扩展功能。
-r, --regexp-extended  在脚本指令中使用扩展正则表达式
-s, --separate      默认情况下,sed将把命令行指定的多个文件名作为一个长的连续的输入流。而GNU sed则允许把他们当作单独的文件,这样如正则表达式则不进行跨文件匹配。
-u, --unbuffered    最低限度的缓存输入与输出。

 

以上仅是sed程序本身的选项功能说明,至于具体的脚本指令(即对文件内容做的操作)后面我们会详细描述,这里就简单介绍几个脚本指令操作作为sed程序的例子。

a,append        追加
i,insert        插入
d,delete        删除
s,substitution  替换

如:$ sed "2a itcast" ./testfile 在输出testfile内容的第二行后添加"itcast"。

$ sed "2,5d" testfile

sed处理的文件既可以由标准输入重定向得到,也可以当命令行参数传入,命令行参数可以一次传入多个文件,sed会依次处理。sed的编辑命令可以直接当命令行参数传入,也可以写成一个脚本文件然后用-f参数指定,编辑命令的格式为

/pattern/action

其中pattern是正则表达式,action是编辑操作。sed程序一行一行读出待处理文件,如果某一行与pattern匹配,则执行相应的action,如果一条命令没有pattern而只有action,这个action将作用于待处理文件的每一行。

常用的sed命令

/pattern/p  打印匹配pattern的行     //注意:第一个/一定不能少
/pattern/d  删除匹配pattern的行
/pattern/s/pattern1/pattern2/   查找符合pattern的行,将该行第一个匹配pattern1的字符串替换为pattern2
/pattern/s/pattern1/pattern2/g  查找符合pattern的行,将该行所有匹配pattern1的字符串替换为pattern2

 

使用p命令需要注意,sed是把待处理文件的内容连同处理结果一起输出到标准输出的,因此p命令表示除了把文件内容打印出来之外还额外打印一遍匹配pattern的行。比如一个文件testfile的内容是
123
abc
456

打印其中包含abc的行

$ sed '/abc/p' testfile
123
abc
abc
456

要想只输出处理结果,应加上-n选项,这种用法相当于grep命令

$ sed -n '/abc/p' testfile
abc

使用d命令就不需要-n参数了,比如删除含有abc的行

$ sed '/abc/d' testfile
123
456

注意,sed命令不会修改原文件,删除命令只表示某些行不打印输出,而不是从原文件中删去。

使用查找替换命令时,可以把匹配pattern1的字符串复制到pattern2中,比如:

$ sed 's/bc/-&-/' testfile
123
a-bc-
456
pattern2中的&表示原文件的当前行中与pattern1相匹配的字符串

再比如:

$ sed 's/\([0-9]\)\([0-9]\)/-\1-~\2~/' testfile
-1-~2~3
abc
-4-~5~6

pattern2中的\1表示与pattern1的第一个()括号相匹配的内容,\2表示与pattern1的第二个()括号相匹配的内容。sed默认使用Basic正则表达式规范,如果指定了-r选项则使用Extended规范,那么()括号就不必转义了。

$ sed  's/yes/no/;s/static/dhcp/'  ./testfile
注:使用分号隔开指令。

$ sed -e 's/yes/no/' -e 's/static/dhcp/' testfile
注:使用-e选项。

如果testfile的内容是

<html><head><title>Hello World</title></head>
<body>Welcome to the world of regexp!</body></html>

现在要去掉所有的HTML标签,使输出结果为

Hello World
Welcome to the world of regexp!

怎么做呢?如果用下面的命令

$ sed 's/<.*>//g' testfile

结果是两个空行,把所有字符都过滤掉了。这是因为,正则表达式中的数量限定符会匹配尽可能长的字符串,这称为贪心的(Greedy)。比如sed在处理第一行时,<.*>匹配的并不是或这样的标签,而是

<html><head><title>Hello World</title>

这样一整行,因为这一行开头是<,中间是若干个任意字符,末尾是>。那么这条命令怎么改才对呢?留给同学们思考练习。

awk

sed以行为单位处理文件,awk比sed强的地方在于不仅能以行为单位还能以列为单位处理文件。awk缺省的行分隔符是换行,缺省的列分隔符是连续的空格和Tab,但是行分隔符和列分隔符都可以自定义,比如/etc/passwd文件的每一行有若干个字段,字段之间以:分隔,就可以重新定义awk的列分隔符为:并以列为单位处理这个文件。awk实际上是一门很复杂的脚本语言,还有像C语言一样的分支和循环结构,但是基本用法和sed类似,awk命令行的基本形式为:

awk option 'script' file1 file2 ...
awk option -f scriptfile file1 file2 ...

和sed一样,awk处理的文件既可以由标准输入重定向得到,也可以当命令行参数传入,编辑命令可以直接当命令行参数传入,也可以用-f参数指定一个脚本文件,编辑命令的格式为:

/pattern/{actions}
condition{actions}

和sed类似,pattern是正则表达式,actions是一系列操作。awk程序一行一行读出待处理文件,如果某一行与pattern匹配,或者满足condition条件,则执行相应的actions,如果一条awk命令只有actions部分,则actions作用于待处理文件的每一行。比如文件testfile的内容表示某商店的库存量:

ProductA  30
ProductB  76
ProductC  55

打印每一行的第二列:

$ awk '{print $2;}' testfile
30
76
55

自动变量$1、$2分别表示第一列、第二列等,类似于Shell脚本的位置参数,而$0表示整个当前行。再比如,如果某种产品的库存量低于75则在行末标注需要订货:

$ awk '$2<75 {printf "%s\t%s\n", $0, "REORDER";} $2>=75 {print $0;}' testfile
ProductA  30    REORDER
ProductB  76
ProductC  55    REORDER

可见awk也有和C语言非常相似的printf函数。awk命令的condition部分还可以是两个特殊的condition-BEGIN和END,对于每个待处理文件,BEGIN后面的actions在处理整个文件之前执行一次,END后面的actions在整个文件处理完之后执行一次。

awk命令可以像C语言一样使用变量(但不需要定义变量),比如统计一个文件中的空行数

$ awk '/^ *$/ {x=x+1;} END {print x;}' testfile

就像Shell的环境变量一样,有些awk变量是预定义的有特殊含义的:

awk常用的内建变量

FILENAME  当前输入文件的文件名,该变量是只读的
NR  当前行的行号,该变量是只读的,R代表record
NF  当前行所拥有的列数,该变量是只读的,F代表field
OFS 输出格式的列分隔符,缺省是空格
FS  输入文件的列分融符,缺省是连续的空格和Tab
ORS 输出格式的行分隔符,缺省是换行符
RS  输入文件的行分隔符,缺省是换行符

例如打印系统中的用户帐号列表

$ awk 'BEGIN {FS=":"} {print $1;}' /etc/passwd

Linux核心命令

运维内容

  • strace
  • netstat
  • perf
  • top
  • pidstat
  • mpstat
  • dstat
  • vmstat
  • slabtop
  • free
  • top
  • tcpdump
  • ip
  • nicstat
  • dtrace
  • ping
  • dtrace
  • blktrace
  • iptop
  • iostat
  • stap

文本处理类的命令:

  • wc

    wc [option] [file]...
        -l: 统计行数
        -c: 统计字节数
        -w;统计单词数
    
  • tr

    tr: 转换字符或删除字符
        tr '集合1' '集合2'
        tr -d '字符集合'
    
  • cut

    This is a test line.
    -d字符:指定分隔符
    -f#: 指定要显示字段
        单个数字:一个字段
        逗号分隔的多个数字:指定多个离散字段
        -:连续字段,如3-5;
    
  • sort

    按字符进行比较
    sort [option] file...
        -f: 忽略字符大小写;
        -n: 比较数值大小;
        -t: 指定分隔符
        -k: 指定分隔后进行比较字段
        -u: 重复的行,只显示一次;
    
  • uniq

    移除重复的行
    -c:显示每行重复的次数
    -d:仅显示重复过的行
    -u: 仅显示不曾重复的行
    
  • 工具速查链接

    http://linuxtools-rst.readthedocs.io/zh_CN/latest/tool/index.html
    

shell习题训练

  1. 求2个数之和
  2. 计算1-100的和
  3. 将一目录下所有的文件的扩展名改为bak
  4. 编译当前目录下的所有.c文件:
  5. 打印root可以使用可执行文件数,处理结果: root's bins: 2306
  6. 打印当前sshd的端口和进程id,处理结果: sshd Port&&pid: 22 5412
  7. 输出本机创建20000个目录所用的时间,处理结果:

    real    0m3.367s
    user    0m0.066s
    sys     0m1.925s
    
  8. 打印本机的交换分区大小,处理结果: Swap:1024M

  9. 文本分析,取出/etc/password中shell出现的次数

    第一种方法结果:
          4 /bin/bash
          1 /bin/sync
          1 /sbin/halt
         31 /sbin/nologin
          1 /sbin/shutdown
    第二种方法结果:
            /bin/sync       1
            /bin/bash       1
            /sbin/nologin   30
            /sbin/halt      1
            /sbin/shutdown  1
    
  10. 文件整理,employee文件中记录了工号和姓名,(提示join)

    employee.txt:
        100 Jason Smith 
        200 John Doe 
        300 Sanjay Gupta 
        400 Ashok Sharma 
        bonus文件中记录工号和工资
    bonus.txt:
        100 $5,000 
        200 $500 
        300 $3,000 
        400 $1,250 
    要求把两个文件合并并输出如下,处理结果:
        400 ashok sharma $1,250
        100 jason smith  $5,000
        200 john doe  $500
        300 sanjay gupta  $3,000
    
  11. 写一个shell脚本来得到当前的日期,时间,用户名和当前工作目录。

  12. 编写shell脚本获取本机的网络地址。
  13. 编写个shell脚本将当前目录下大于10K的文件转移到/tmp目录下
  14. 编写一个名为myfirstshell.sh的脚本,它包括以下内容。

    a) 包含一段注释,列出您的姓名、脚本的名称和编写这个脚本的目的。
    b) 问候用户。
    c) 显示日期和时间。
    d) 显示这个月的日历。
    e) 显示您的机器名。
    f) 显示当前这个操作系统的名称和版本。
    g) 显示父目录中的所有文件的列表。
    h) 显示root正在运行的所有进程。
    i) 显示变量TERM、PATH和HOME的值。
    j) 显示磁盘使用情况。
    k) 用id命令打印出您的组ID。
    m) 跟用户说“Good bye”
    
  15. 文件移动拷贝,有m1.txt m2.txt m3.txt m4.txt,分别创建出对应的目录,m1 m2 m3 m4 并把文件移动到对应的目录下

  16. root用户今天登陆了多长时间
  17. 终端输入一个文件名,判断是否是设备文件
  18. 统计IP访问:要求分析apache访问日志,找出访问页面数量在前100位的IP数。日志大小在78M左右。以下是apache的访问日志节选

    202.101.129.218 - - [26/Mar/2006:23:59:55 +0800] "GET /online/stat_inst.php?pid=d065 HTTP/1.1" 302 20-"-" "-" "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)"
    
  19. 设计一个Shell程序,在/userdata目录下建立50个目录,即user1~user50,并设置每个目录的权限,其中其他用户的权限为:读;文件所有者的权限为:读、写、执行;文件所有者所在组的权限为:读、执行。

  20. 设计一个shell程序,添加一个新组为class1,然后添加属于这个组的30个用户,用户名的形式为stdxx,其中xx从01到30,并设置密码为对应的stdxx。
  21. 编写shell程序,实现自动删除30个账号的功能。账号名为std01至std30。
  22. 用户清理,清除本机除了当前登陆用户以外的所有用户
  23. 设计一个shell程序,在每月第一天备份并压缩/etc目录的所有内容,存放在/root/bak目录里,且文件名,为如下形式yymmdd_etc,yy为年,mm为月,dd为日。Shell程序fileback存放在/usr/bin目录下。
  24. 对于一个用户日志文件,每行记录了一个用户查询串,长度为1-255字节,共几千万行,请排出查询最多的前100条。 日志可以自己构造。 (提示:awk sort uniq head)
  25. 编写自己的ubuntu环境安装脚本
  26. 编写服务器守护进程管理脚本。
  27. 查看TCP连接状态

    netstat -nat |awk ‘{print $6}’|sort|uniq -c|sort -rn
    
    netstat -n | awk ‘/^tcp/ {++S[$NF]};END {for(a in S) print a, S[a]}’ 或
    netstat -n | awk ‘/^tcp/ {++state[$NF]}; END {for(key in state) print key,"\t",state[key]}’
    netstat -n | awk ‘/^tcp/ {++arr[$NF]};END {for(k in arr) print k,"t",arr[k]}’
    
    netstat -n |awk ‘/^tcp/ {print $NF}’|sort|uniq -c|sort -rn
    
    netstat -ant | awk ‘{print $NF}’ | grep -v ‘[a-z]‘ | sort | uniq -c
    
  28. 查找请求数请20个IP(常用于查找攻来源):

    netstat -anlp|grep 80|grep tcp|awk ‘{print $5}’|awk -F: ‘{print $1}’|sort|uniq -c|sort -nr|head -n20
    
    netstat -ant |awk ‘/:80/{split($5,ip,":");++A[ip[1]]}END{for(i in A) print A[i],i}’ |sort -rn|head -n20
    
  29. 用tcpdump嗅探80端口的访问看看谁最高

    tcpdump -i eth0 -tnn dst port 80 -c 1000 | awk -F"." ‘{print $1"."$2"."$3"."$4}’ | sort | uniq -c | sort -nr |head -20
    
  30. 查找较多time_wait连接

    netstat -n|grep TIME_WAIT|awk ‘{print $5}’|sort|uniq -c|sort -rn|head -n20
    
  31. 找查较多的SYN连接

    netstat -an | grep SYN | awk ‘{print $5}’ | awk -F: ‘{print $1}’ | sort | uniq -c | sort -nr | more
    
  32. 根据端口列进程

    netstat -ntlp | grep 80 | awk ‘{print $7}’ | cut -d/ -f1
    
  33. 获得访问前10位的ip地址

    cat access.log|awk ‘{print $1}’|sort|uniq -c|sort -nr|head -10
    cat access.log|awk ‘{counts[$(11)]+=1}; END {for(url in counts) print counts[url], url}’
    
  34. 访问次数最多的文件或页面,取前20

    cat access.log|awk ‘{print $11}’|sort|uniq -c|sort -nr|head -20
    
  35. 列出传输最大的几个exe文件(分析下载站的时候常用)

    cat access.log |awk ‘($7~/.exe/){print $10 " " $1 " " $4 " " $7}’|sort -nr|head -20
    
  36. 列出输出大于200000byte(约200kb)的exe文件以及对应文件发生次数

    cat access.log |awk ‘($10 > 200000 && $7~/.exe/){print $7}’|sort -n|uniq -c|sort -nr|head -100
    
  37. 如果日志最后一列记录的是页面文件传输时间,则有列出到客户端最耗时的页面

    cat access.log |awk ‘($7~/.php/){print $NF " " $1 " " $4 " " $7}’|sort -nr|head -100
    
  38. 列出最最耗时的页面(超过60秒的)的以及对应页面发生次数

    cat access.log |awk ‘($NF > 60 && $7~/.php/){print $7}’|sort -n|uniq -c|sort -nr|head -100
    
  39. 列出传输时间超过 30 秒的文件

    cat access.log |awk ‘($NF > 30){print $7}’|sort -n|uniq -c|sort -nr|head -20
    
  40. 统计网站流量(G)

    cat access.log |awk ‘{sum+=$10} END {print sum/1024/1024/1024}’
    
  41. 统计404的连接

    awk ‘($9 ~/404/)’ access.log | awk ‘{print $9,$7}’ | sort
    
  42. 统计http status

    cat access.log |awk ‘{counts[$(9)]+=1}; END {for(code in counts) print code, counts[code]}'
    cat access.log |awk '{print $9}'|sort|uniq -c|sort -rn
    
  43. 蜘蛛分析,查看是哪些蜘蛛在抓取内容。

    /usr/sbin/tcpdump -i eth0 -l -s 0 -w - dst port 80 | strings | grep -i user-agent | grep -i -E 'bot|crawler|slurp|spider'
    
  44. 创建一个用户mandriva,其ID号为2002,基本组为distro(组ID为3003),附加组为linux;

    # groupadd linux
    # groupadd -g 3003 distro
    # useradd -u 2002 -g distro -G linux mandriva
    
  45. 创建一个用户fedora,其全名为Fedora Community,默认shell为tcsh; # useradd -c "Fedora Community" -s /bin/tcsh fedora

  46. 修改mandriva的ID号为4004,基本组为linux,附加组为distro和fedora;

    # usermod -u 4004 -g linux -G distro,fedora mandriva
    
  47. 给fedora加密码,并设定其密码最短使用期限为2天,最长为50天;

    # passwd fedora
    # chage -m 2 -M 50 fedora
    
  48. 调试命令

    strace -p pid
    
  49. 写一个脚本

    1、创建一个组newgroup, id号为4000;
    2、创建一个用户mageedu1, id号为3001,附加组为newgroup;
    3、创建目录/tmp/hellodirxyz
    4、复制/etc/fstab至上面的目录中
    5、改变目录及内部文件的属主和属组为mageedu1;
    6、让目录及内部文件的其它用户没有任何权限;
    
            #!/bin/bash
            # Description:
            # Version:
            # Datetime:
            # Author:
    
            myGroup="newgroup1"
            myUser="mageedu2"
            myDir="/tmp/hellodirxyz1"
            myID=3002
    
            groupadd -g 4001 $myGroup
            useradd -u $myID -G $myGroup $myUser
            mkdir $myDir
            cp /etc/fstab $myDir
            chown -R $myUser:$myUser $myDir
            chmod -R o= $myDir
    
            unset myGroup myUser myID myDir
    
  50. 统计/bin、/usr/bin、/sbin和/usr/sbin等各目录中的文件个数;

    # ls /bin | wc -l
    
  51. 显示当前系统上所有用户的shell,要求,每种shell只显示一次;

    # cut -d: -f7 /etc/passwd | sort -u
    
  52. 取出/etc/passwd文件的第7行;

    # head -7 /etc/passwd | tail -1
    
  53. 显示第3题中取出的第7行的用户名;

    # head -7 /etc/passwd | tail -1 | cut -d: -f1
    
    # head -7 /etc/passwd | tail -1 | cut -d: -f1 | tr 'a-z' 'A-Z'
    
  54. 统计/etc目录下以P或p开头的文件个数;

    # ls -d /etc/[Pp]* | wc -l
    
  55. 写一个脚本,用for循环实现显示/etc/init.d/functions、/etc/rc.d/rc.sysinit和/etc/fstab各有多少行;

    for fileName in /etc/init.d/functions /etc/rc.d/rc.sysinit /etc/fstab; do
        wc -l $fileName
    done
    
    #!/bin/bash
    for fileName in /etc/init.d/functions /etc/rc.d/rc.sysinit /etc/fstab; do
        lineCount=`wc -l $fileName | cut -d' ' -f1`
        echo "$fileName: $lineCount lines."
    done
    
    #!/bin/bash
    for fileName in /etc/init.d/functions /etc/rc.d/rc.sysinit /etc/fstab; do
        echo "$fileName: `wc -l $fileName | cut -d' ' -f1` lines."
    done
    
  56. 写一个脚本,将上一题中三个文件的复制到/tmp目录中;用for循环实现,分别将每个文件的最近一次的修改时间改为2016年12月15号15点43分;

    for fileName in /etc/init.d/functions /etc/rc.d/rc.sysinit /etc/fstab; do
        cp $fileName /tmp
        baseName=`basename $fileName`
        touch -m -t 201109151327 /tmp/$baseName
    done
    
  57. 写一个脚本, 显示/etc/passwd中第3、7和11个用户的用户名和ID号;

    for lineNo in 3 7 11; do
        userInfo=`head -n $lineNo /etc/passwd | tail -1 | cut -d: -f1,3`
        echo -e "User: `echo $userInfo | cut -d: -f1`\nUid: `echo $userInfo |cut -d: -f2`"
    done
    
  58. 显示/proc/meminfo文件中以大小写s开头的行;

    # grep "^[sS]" /proc/meminfo
    # grep -i "^s" /proc/meminfo
    
  59. 取出默认shell为非bash的用户;

    # grep -v "bash$" /etc/passwd | cut -d: -f1
    
  60. 取出默认shell为bash的且其ID号最大的用户;

    # grep "bash$" /etc/passwd | sort -n -t: -k3 | tail -1 | cut -d: -f1
    
  61. 显示/etc/rc.d/rc.sysinit文件中,以#开头,后面跟至少一个空白字符,而后又有至少一个非空白字符的行;

    # grep "^#[[:space:]]\{1,\}[^[:space:]]\{1,\}" /etc/rc.d/rc.sysinit
    
  62. 显示/boot/grub/grub.conf中以至少一个空白字符开头的行;

    # grep "^[[:space:]]\{1,\}[^[:space:]]\{1,\}" /boot/grub/grub.conf
    
  63. 找出/etc/passwd文件中一位数或两位数;

    # grep --color=auto "\<[0-9]\{1,2\}\>" /etc/passwd
    
  64. 找出ifconfig命令结果中的1到255之间的整数;

    # ifconfig | grep -E --color=auto "\<([1-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\>"
    
  65. 查看当前系统上root用户的所有信息;

    # grep "^root\>" /etc/passwd
    
  66. 添加用户bash和testbash、basher,而后找出当前系统上其用户名和默认shell相同的用户;

    # grep --color=auto "^\([[:alnum:]]\{1,\}\)\>.*\1$" /etc/passwd
    
  67. 找出netstat -tan命令执行的结果中以“LISTEN”或“ESTABLISHED”结尾的行;

  68. 取出当前系统上所有用户的shell,要求:每种shell只显示一次,且按升序显示;
    # cut -d: -f7 /etc/passwd | sort -u
    

自动化

开机自启动脚本

如果要添加为开机启动执行的脚本文件,可先将脚本复制或者软连接到/etc/init.d/目录下,然后用:

    update-rc.d xxx defaults NN命令(NN为启动顺序),

将脚本添加到初始化执行的队列中去。

注意如果脚本需要用到网络,则NN需设置一个比较大的数字,如99。

1) 将你的启动脚本复制到 /etc/init.d目录下,以下假设你的脚本文件名为 test。

2) 设置脚本文件的权限

    $ sudo chmod 755 /etc/init.d/test

3) 执行如下命令将脚本放到启动脚本中去:

    $ cd /etc/init.d
    $ sudo update-rc.d test defaults 95

posted on 2018-05-08 10:35  maogefff  阅读(647)  评论(0编辑  收藏  举报

导航