• 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
    
  • 相关阅读:
    线程中断总结
    线程的基本协作和生产者消费者
    synchronized总结
    线程基础总结
    Java集合总结(三):堆与优先级队列
    Java集合总结(二):Map和Set
    Java集合总结(一):列表和队列
    java枚举类型总结
    java 内部类简单总结
    java Integer包装类装箱的一个细节
  • 原文地址:https://www.cnblogs.com/fortunely/p/14590618.html
Copyright © 2020-2023  润新知