Linux 系统编程学习笔记

Shell简介

shell用于解释执行用户的命令。
shell 2种执行方式:

  1. 用户输入一条,解释执行一条;
  2. 事先写一个shell脚本,包含多条命令,shell一次执行完毕,这种方式也叫批处理(batch);

shell的多个版本:

  1. sh(Bourne Shell):Steve Bourne开发,各种UNIX系统配有;
  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开发,目标是与POSIX标准保持一致,同时兼顾对sh的兼容,也从csh和ksh借鉴了很多功能,各种Linux发行版标准配置;

查看已知(但不一定安装)的Shell:文件/etc/shells

$ cat /etc/shells

用户默认Shell设置位于/etc/passwd文件,打开图形终端窗口自动执行/bin/bash的配置就在该文件。

切换到其他Shell,用sh命令
Linux中默认的shell如何切换为其他类型的shell

Shell如何执行命令

执行交互式命令

用户输入命令后,一般情况下Shell会fork并exec该命令,Shell内建命令除外(如cd,alias,umask,exit)。执行内建命令相当于调用Shell进程中的一个函数,并不创建新的进程。
内建命令不穿就新进程,但会有Exit Status(状态码),0标识成功,非0表示失败,可以用特殊变量 $? 读出。
查看内建命令

$ man bash-builtins

cd命令为什么要实现成内建命令?而不用独立的程序?
因为Shell独立程序是通过fork新建子进程,然后让子进程exec转去执行命令对应的独立程序,但是子进程当前目录路径的改变并不会影响父进程,子进程结束又回到父进程shell环境了,这样无法实现当前shell路径的改变。
同样的,改变当前shell的参数和环境变量表内的都需要使用内建命令。
参考cd命令为何要实现成shell内建命令

执行脚本

编写脚本script.sh:

#! /bin/sh
cd ..
ls

# 表示注释
#! (Shebang)位于文件开头第一行,则表示该脚本使用后面指定的解释器/bin/sh解释执行

如果把该脚本加上可执行权限,然后执行

$ chmod + x script.sh
$ ./script.sh

Shell会fork一个子进程,并调用exec执行./script.sh脚本程序,exec系统调用把子进程的代码替换成./script.sh程序代码段,并从_start开始执行。
如果要执行的是一个文本文件,并且第一行用Shebang指定了解释器,则用解释器程序代码段替换当前进程,并且从解释器的_start开始执行,而该文本文件被当做命令行参数传给解释器。
执行./script.sh <=>

$ /bin/sh ./script.sh

以这种方式执行,是不需要script.sh文件具有可执行权限(x)的。
假如script以#! /bin/sed -f 开头,执行脚本相当于执行

$ /bin/sed -f ./script.sh

下面这两种执行shell脚本方法是等价的:

$ ./script.sh
$ sh ./script.sh

执行script脚本步骤:

  1. 交互Shell(bash)fork/exec一个子Shell(sh)用于脚本执行,父进程bash等待子进程sh终止;
  2. sh读取脚本中的cd ..命令,调用相应的函数执行内建命令,改变当前工作目录为上一级目录;
  3. sh读取脚本中的ls命令,for/exec该程序,列出当前工作目录下的文件,sh等待ls终止;
  4. ls终止后,sh继续执行,读到脚本文件末尾,sh终止;
  5. sh终止后,bash继续执行,打印提示符等待用户输入;

如果将命令行下输入的命令用过() 括起来和不括起来是不一样的,括起来的会fork出一个Shell执行括号中的命令。一行中可以输入由分号 ";"隔开的多个命令,比如

$ (cd ..;ls -l) # shell命令,等同于script脚本执行效果

$ cd ..;ls -l  # shell命令,不同于上面的命令,因为是直接在交互式shell下执行的,会改变shell的pwd(当前目录)

# 等价于这样执行Shell脚本
$ source ./script.sh # source 是shell内建命令,不会创建子shell,而是直接在交互式shell下逐行执行脚本命令
# or
$ . ./script.sh

示例:

$ (exit 2)  # fork子shell执行exit命令,返回状态码2
$ echo $? # 在当前shell获取上一次运行命令的结束代码

Shell的基本语法

变量

惯例:Shell变量由全大写字母 + 下划线组成。
两种类型Shell变量:

  • 环境变量
    环境变量可以从父进程传递给子进程,Shell进程的环境变量可以传递fork的子进程。printenv命令可以显示当前Shell进程的环境变量。

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

环境变量是任何进程都有的概念,本地变量是Shell特有的概念。
Shell中,定义或赋值一个变量

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

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

$ export VARNAME=value # 定义本地变量,并导出为环境变量
# <=>
$ VARNAME=value
$ export VARNAME

unset命令删除变量

$ unset VARNAME

({VARNAME}读取变量值,没有歧义时可以简化成)VARNAME

# 注意它们的区别
$ echo $SHELL       # 变量名SHELL
$ echo $SHELLabc   # 变量名SHELLabc
$ echo $SHELL abc  # 变量名SHELL,abc是参数
$ echo ${SHELL}abc # 变量名SHELLabc

注意:Shell变量的值都是字符串,无需专门指定类型;Shell变量不需要先定义,后使用,对于未定义变量取值为空字符串。

文件名替换(Globbing):*>[]

用于匹配的字符称为通配符(Wildcard)

* 匹配0个或多个任意字符
? 匹配一个任意字符
[若干字符] 匹配方括号中任意一个字符的一次出现
$ ls /dev/ttyS* # ls参数:当前目录下以/dev/ttyS为前缀的文件名
$ ls ch0?.doc   # ls参数:ch0+一个字符的doc文件名
$ ls ch0[0-2].doc # ls参数:ch0 + 0/1/2的doc文件名
$ ls ch[012][0-9].doc # ls参数:ch + 012任一字符出现一次 + 0~9任一字符出现一次的doc文件名

命令代换:`或$()

`或$()括起来的也是一条命令,shell先执行该命令,然后将输出结果立刻代换到当前命令行中。
如定义一个存放date命令的输出

# 命令代换 用` `表示
$ DATE=`date` # `date` 命令输出结果,代换到当前命令中
$ echo $DATE
#<=> 用$()表示
$ DATE=$(date)

算术代换:$(())

用于算术计算,((())中Shell变量取值将转换为整数。)(())只能用+-*/和()运算符,而且只能做整数运算

$ VAR=45
$ echo $(($VAR+3))

转义字符

Shell中用作转义字符,用于去紧跟后面的单个字符的特殊意义(回车除外)。简单来说,就是把后面紧跟字符当做字面量。

$ echo $SHELL # 打印SHELL变量值
/bin/bash

$ echo $SHELL # 打印字面量 $SHELL
$SHELL

$ echo \         # 打印字面量 


# 创建名为$ $的文件
$ touch $ $

特殊符号 -,即使加上转义还是报错,会被当做命令行参数选项,不会当做文件名。解决办法:

$ touch -hello # 会报错

# 正确的创建名为-hello的2种方法
$ touch ./-hello # 创建名为-hello的文件
# <=>
$ touch -- -hello

+ 回车,表示续行

$ ls  # 续行命令
>-l    # > 是shell给出的续行提示符
# <=> $ ls -l

单引号

和C不同,shell中单引号、双引号都是字符串的界定符,而不是字符的界定符。
注意:

  1. 单引号(' ')不同于算术代换( );
  2. 字符串中,不能出现单引号;
  3. 引号需要配对,如果不配对,shell会给出续行提示符(>);
$ echo '$SHELL'
$SHELL # 打印结果

$ echo 'ABC回车
> DE'回车
ABC
DE

双引号

双引号用于保持引号内所有字符的字面值(包括回车),但以下几种情况除外:

  • 反引号仍表示命令替换
  • $表示$的字面值
  • ' 表示'的字母值
  • " 表示"的字面值
  • 表示的字面值
  • 除以上情况外,其他字符前面的无特殊含义,只表示字面值
$ echo "$SHELL"
/bin/bash
$ echo "`date`"

$ echo "I'd say: "Go for it""
I'd say: "Go for it"
$ echo ""回车
>"回车 
"

$ echo "\"

bash 启动脚本

启动脚本是bash启动时自动执行的脚本。
启动脚本有什么用?
用户可以把一些环境变量的设置和alias、umask设置放在启动脚本中,这样每次启动Shell时这些设置自动生效。

启动bash的方法不同,执行启动脚本的步骤也不同:

作为交互登录Shell启动,或者使用--login参数启动

交互Shell是指用户在提示符下输命令的Shell,而非执行脚本的Shell,登录Shell是在输入用户名和密码登录后得到的Shell,如从字符终端等了或者用telnet/ssh从远程登录,但是从图形界面的窗口管理器登录之后会显示桌面而不会产生登录Shell(也不会执行启动脚本),在图形界面下打开终端窗口得到的Shell也不是登录Shell。

启动bash时如何自动执行脚本?

  1. 先执行/etc/profile,系统中每个用户登录时都要执行该脚本,如果系统管理员系统设置对所有用户都生效,可以写在这个脚本里;
  2. 然后依次查找当前用户主目录的~/.bash_profile, /.bash_login和/.profile三个文件,找到第一个存在并且可读的文件来执行。
    如果希望某个设置只对当前用户生效,可以写在这个脚本里,由于这个脚本中/tec/profile之后执行,/etc/profile设置的环境变量的值在这个脚本里可以修改,覆盖系统中的全局设置。
    /.profile启动脚本是sh规定的,bash规定首先查找以/.bash开头的启动脚本,如果没有执行~/.profile,是为了和sh保持一致。
  3. 退出登录时,执行~/.bash_logout脚本(如果存在)。

以交互非登录Shell启动

图形界面下开一个终端窗口,或者在登录Shell提示符下在输入bash命令,得到一个交互式非登录的Shell。这种Shell在启动时自动执行~/.bashrc脚本。

为使登录Shell也能自动执行/.bashrc,通常在/.bash_profile中调用~/.bashrc

# 如果~/.bashrc文件存在则source它
if [ -f ~/.bashrc ]; then
    . ~/.bashrc
fi

多数Linux发行版在创建账户时会自动创建/.bash_profile和/.bashrc脚本,~/.bash_profile中通常有上面这几行
如果要在启动脚本中做某些 ,使它在图形终端窗口和字符终端的Shell中都起作用,最好就是在~/.bashrc中设置。

实验,在~/.bashrc文件末尾添加一行:

export PATH=$PATH:/home/akaedu

然后关掉终端串口重新打开,或者从字符终端logout之后重新登录,主目录下面的程序就可以直接输入程序名,而不必输入路径了,如:

$ a.out
# 不必带路径
$ ./a.out

为什么要区分登录Shell和非登录Shell?
因为最初的设计是这样考虑的:如果从字符终端或远程登录,那么等你Shell是该用户的所有其他进程的父进程,也是其他子Shell的父进程,所以环境变量值登录Shell的启动脚本里设置一次就可以自动带到其他非登录Shell里,而本地变量、函数、alias等设置没有办法带到子Shell里,需要每次启动非登录Shell时再设置一遍,所以就需要有非登录Shell的启动脚本。
一遍建议:
~/.bash_profile 里设置环境变量;
~/.bashrc设置本地变量、函数、alias等;

如果Linux带有图形系统,则环境变量应该准~/.bashrc里设置,因为图形界面的窗口管理器登录不会产生登录Shell。

非交互启动

为执行脚本fork出来的子Shell是非交互Shell,启动时执行的脚本文件由环境变量BASH_EN定义,相当于自动执行命令:

if [ -n "$BASH_ENV" ]; then . "$BASH_ENV"; fi`

如果环境变量BASH_EN的值不是空字符串,则把它的值当作启动脚本的文件名,source这个脚本。

以sh命令启动

如果以sh命令启动bash,bas将模拟sh的行为,以~/.bash_开头的那些启动脚本就不认了。所以,如果作为交互登录Shell启动,或者使用--login参数启动,则一次执行下面的脚本:

  1. /etc/profile
  2. ~/.profile

如果作为交互Shell启动,相当于自动执行:

if [ -n "$ENV" ]; then . "$ENV"; fi`

如果作为非交互Shell启动,则不执行任何启动脚本。通常以'#! /bin/sh' 开头的Shell脚本都属于这种方式

Shell脚本语法

条件测试 test [

命令test或[ 可以测试一个条件是否成立,如果测试结果为真,则该命令的Exit Status = 0;如果为假,则Exit Status = 1(注意不是C true/false)。
例如,测试两个数的大小关系:

$ VAR=2
$ test $VAR -gt 1 # 测试 $VAR > 1
$ echo $?
0
$ test $VAR -gt 3 # 测试 $VAR > 3
$ echo $?
1
$ [ $VAR -gt 3]    # 测试 $VAR > 3  # $VAR, -gt, 3, ]是[命令的4个参数
$ echo $?
1

test命令和[ 命令的区别是:test命令不需要] 参数。以[命令为例,常见测试命令:

-gt 表示进行 > (大于测试)。其他参数的含义见下表:

例子

$ VAR=abc
$ [ -d Desktop -a $VAR = 'abc' ] # 测试:如果目录Desktop存在,而且变量VAR='abc'
$ echo $?
0

如果$VAR没有事先定义,则展开为空串,会造成测试条件语法错误
验证测试:

$ unset VAR
$ [ -d Desktop -a $VAR = 'abc' ] # 由于已经取消变量VAR,shell展开为空串,[ 测试语法错误
bash: [: too many arguments
$ [ -d Desktop -a "$VAR"  = 'abc' ] # 将变量写作" "内,即使是空串,也不会报错
$ echo $?
1

建议:应该总是变量取值放在双引号之中,避免展开为空串导致错误

if/then/elif/else/fi

分支控制。

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

其实是3条命令:
第一条,if [ -f ~/.bashsrc ]
第二条,then .~/.bashsrc
第三条,fi
两条命令写在同一行需要用; (分号)隔开。then后有换行,但命令没有写完,Shell自动续行。
if命令的参数组成一条子命令,如果Exit Status = 0(真),则执行then后面子命令;如果 Exit Status ≠ 0(假),则执行elif、else、fi后面的命令。
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 = 0(真)。也可以执行/bin/true or /bin/false分别代表真 or 假。

#! /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变量

*&&和||语法
&& 相当于 "if...then...", || 相当于"if not...then..."。
注意区别:-a和-o仅用于测试表达式连接2个测试条件,-a中测试条件中表示逻辑and,-o表示逻辑or
在测试语句中,它们是等价的,如

test "$VAR" -gt 1 - a "$AVR" -lt 3
<=>
test "$VAR" -gt 1 && test "$VAR" -lt 3

case/esac

case命令可类比C的switch/case,esac表示case语句块的结束。C语言case只能匹配整型或字符型常量表达式,Shell脚本的case可以匹配字符串和Wildcard。每个匹配分支可以有若干条命令,末尾必须以 ;; (2个分号)结束。执行时,直接跳到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中找到。

启动apache2服务命令:

$ sudo /etc/init.d/apache2 start

$1 是一个特殊变量,在执行脚本时自动取值为第一个命令行参数,也就是start。同理,命令行参数指定stop, reload or restart也可以进入其他分支。

for/do/done

Shell 脚本循环结构不同于C,类似于C++的foreach。

#! /bin/sh

for FRUIT in apple banna pear; do # FRUIT在in后面的列表中循环取值
    echo "I like $FRUIT"
done

示例,将指定目录chap0/1/2/...下的所有文件,其名称后面加上~(表示临时文件)

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

# <=>
$ for FILENAME in `ls charp?`; do mv $FILENAME $FILENAME~; done

while/do/done

while用法类似C。
例,验证密码的脚本

#! /bin/sh

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

通过算术运算控制循环次数

#! /bin/sh

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

位置参数和特殊变量

Shell自动赋值的特殊变量

位置参数可以用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 paramter is $1"
ehco "The second parameter is $2"
echo "The parameter list is $@"
shift # <=> shift 1
echo "The first parameter is $1"
echo "The second parameter is $2"
echo "The parameter list is $@"

函数

Shell也有函数的概念,但是函数定义中没有返回值也没有参数列表。

#! /bin/sh
foo() { echo "Function foo is called"; }
echo "-=start=-"
foo
echo "-=end=-"

注意函数体左花括号{ 和后面的命令之间必须有空格或换行,如果将最后一条命令和右花括号写在同一行,命令末尾必须有; (分号)
定义函数foo() 并不执行,只有在调用的时候才执行。
Shell 函数没有参数列表,但是可以传参,函数体用位置参数$0, $1, $2等来提取传入参数。函数中的位置参数是函数局部变量,不会影响函数外面的$0, $1, $2等变量。函数可以用return命令返回,后面如果带数字表示函数的Exit Status。

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

#! /bin/sh

# true:返回0;false:返回1
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

Shell脚本的调试方法

Shell提供一些用于调试脚本的选项,如:
-n 读一遍脚本中的命令但不执行,用于检查脚本中的语法错误

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

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

使用这些选项的3种方法:

  1. 命令行提供参数
$ sh -x ./script.sh
  1. 脚本开头提供参数
#! /bin/sh -x
  1. 脚本中用set命令启用或禁用参数
#! /bin/sh
if [ z "$1" ]; then
    set -x # 启用 -x参数
    echo "ERROR: Insufficient Args."
    exit 1
    set +x # 禁用 -x参数
fi
原文地址:https://www.cnblogs.com/fortunely/p/14590618.html