Shell

189 阅读6分钟

一、概述

shell,命令行解释器。 大部分命令都是通过C语言写的,编译成可执行文件,通过命令执行。 胶水语言,把一些操作,一些命令粘到一起。
shell的类型:-zsh(目前macOS默认), -bash-3.2。牵扯到跨平台 交互式登陆和非登录, 终端输入zsh,输出带-表示登陆状态,不带非登陆。 配置环境变量。\

解释器:源代码通过解释器生成通用语言,中间代码出现问题不会影响后面代码的执行。\

shell起始行是固定的,#!/bin/bash。Shebang(Hashbang),一个由#!构成的字符序列,出现在文本文件的第一行。操作系统的程序加载器会分析Shebang后的内容,将这些内容作为解释器指令,并调用该指令,并将载有Shebang的文件路径作为该解释器的参数。

#!/usr/bin/python
#!/usr/bin/env python

env: 不同操作系统,脚本解释器可能被安装于系统的不同的目录,设置到系统的PATH中,env可以在系统的PATH目录中找到查看。上述命令,使用在用户路径中找到的第一个Python版本,但是可以通过指定版本号:#!/usr/bin/env pythonX.x

常用命令参考
参考1
参考2

二、特殊符号的使用:

  • shell通过空格来分隔参数;
  • "":双引号常用语包含一组字符串,在双引号中,除了"$","\",反引号有特殊含义,其余字符没有特殊含义;
  • '':单引号的功能与双引号类似,不过单引号的所有字符都没有特殊含义;
  • 反引号:命令替换,内容通常是命令行,程序会优先执行内容,并使用运行结果替换掉反引号中的内容;
  • $():同反引号;
  • ():定义一个数组变量;
  • $(()):算数运算,内容为数学表达式;
  • (()):允许在比较过程中使用高级数学表达式;
  • $[]:同$(())
  • []:用作条件测试,和test命令一样;
  • ${}:变量引用,类似于$,但是范围更精准;
  • {}:用于括起一个语句块;
  • [[]]:提供针对字符串比较的高级特性,使用该符号对字符串进行比较时,可以把右边的项看做一个模式,故而可以在[[]]中使用正则表达式;
  • ::作为内建命令,占位符,参数扩展,重定向;
  • -- 代表命令不再有参数拼接了,后面的内容都是直接给命令传递的;
  • |管道,处理前一个命令传过来的正确信息,将正确信息作为标准输入传给下一个命令。xargs将标准输入转为命令行参数。
  • echo:[-ne][字符串]:按照规则打印字符串
-n 不要在最后自动换行
-e 若字符串中出现以下字符,则特别加以处理
    \a 发出警告;
    \b 删除前一个字符;
    \c 不产生进一步输出 (\c 后面的字符不会输出);
    \f 换行但光标仍旧停留在原来的位置;
    \n 换行且光标移至行首;
    \r 光标移至行首,但不换行;
    \t 插入tab;
    \v 与\f相同;
    \\ 插入\字符;
    \nnn 插入 nnn(八进制)所代表的ASCII字符;
用echo命令打印带有色彩的文字:
    文字色:
        echo -e "\e[1;31mThis is red text\e[0m"
            \e[1;31m 将颜色设置为红色
            \e[0m 将颜色重新置回
            颜色码:重置=0,黑色=30,红色=31,绿色=32,黄色=33,蓝色=34,洋红=35,青色=36,白色=37
    背景色:
        echo -e "\e[1;42mGreed Background\e[0m"
        颜色码:重置=0,黑色=40,红色=41,绿色=42,黄色=43,蓝色=44,洋红=45,青色=46,白色=47
    文字闪动:
        echo -e "\033[37;31;5mLogic Cat 帅帅帅...\033[39;49;0m"
        红色数字处还有其他数字参数:0 关闭所有属性、1 设置高亮度(加粗)、4 下划线、5 闪烁、7 反显、8 消隐
  • type:显示命令属性,会显示该命令所在的位置。

三、标准输入输出,错误

  • 标准输入(stdin):代码为0,使用<<<;
    1. <标准输入(将文件的内容传递到命令的标准输入);
    2. <<结束输入,后接一个结束输入符(将带有结束标志的文档内容传递到命令的标准输入);
    3. <<<将右侧的字符串传递到左侧命令的标准输入。
  • 标准输出(stdout):代码为1,使用>>>;
    1. 1> 以覆盖的方式将正确的数据输出到指定到文件或设备;
    2. 1>> 以累加到方法将正确到数据输出到指定到文件或者设备上。
  • 标准错误输出(stderr):代码为2,使用2>或2>>;
    1. 2> 以覆盖的方式将错误的数据输出到指定到文件或设备;
    2. 2>> 以累加的方式将错误的数据输出到指定到文件或设备;
    3. 2>/dev/null 将错误数据丢弃,只显示正确数据;
    4. 2>&1 或者 &>将正确的数据和错误的数据写入同一个文件;
    5. 1>&2 正确返回值传递给2输出通道, &2表示2输出通道;
    6. 2>&1 错误返回值传递给1输出通道, 同样&1表示1输出通道。
  • cmd 不考虑命令相关性,连续执行;
    1. 当前一个命令执行成功会回传一个$?=0的值;
    2. cmd1 && cmd2 如果第一个命令的$?0,则执行第二个命令;
    3. cmd1 || cmd2 如果第一个命令的$?0,则不执行第二个命令。否则执行第二个命令。
  • 管道仅能处理前面一个命令传来的正确信息,将正确信息作为标准输入传给下一个命令。
    1. 管道命令只处理前一个命令正确输出,不处理错误输出;
    2. 管道命令右边命令,必须能够接收标准输入流命令才行;
    3. 大多数命令都不接受标准输入作为参数,只能直接在命令行输入参数,这导致无法用管道命令传递参数。
  • -减号在一些命令中表示从标准输入中获取内容;
  • xargs是将标准输入转为命令行参数。

四、执行shell的三种方式:

  1. 通过./xxx.sh,等同sh xxx.shbash xxx.shzsh xxx.sh。执行脚本时,当前shell是父进程,生成一个子shell进程,在在子shell中执行脚本,脚本执行完毕,退出shell,回到当前shell。也叫fork方式。
  2. 通过source xxx.sh的方式,在当前上下文中执行脚本,不会生成新的进程,脚本执行完毕,回到当前shell。. xxx.shsource xxx.sh等效,不需要执行权限;
  3. 通过execc ./xxx.sh的方式,会把当前进程替换成command进程,并且保持PID不变,执行完毕,直接退出,不回到之前的shell环境。

其中需要权限的方式:./xxx.shexec xxx.sh
不需要权限的方式:sh xxx.sh, /bin/bash xxx.sh, /bin/zsh xxx.sh,source xxx.sh

五、声明变量

  • 变量默认为字符串,不关心这个串啥含义。
  • 默认的数值运算是整数类型,进行数学运算,必须使用一些例如declare,expr,双括号等的命令。
  • 变量可分为两类:a.局部变量,只在创建的它们的shell中可用,在函数内定义,函数执行后就被删除;b.环境变量,可以在创建它们的shell及其派生出的来的任意子进程中使用,在整个脚本执行期间,只要没有被删除就会一直存在。
  • 定义规则:变量名必须以字母或下划线字符开头,其余的字符可以是字母、数字0~9或下划线字母。任何其他的字符都标志着变量名的终止,大小写敏感。
  • 给变量赋值时,等号周围不能有任何空白符;
  • 通常大写字符为系统默认变量,个人习惯;
  • set:查看所有变量(含环境变量与自定义变量),以及设置shell变量的新变量值。
-a:标示已修改的变量,以供输出至环境变量。
-b:使被中止的后台程序立刻回报执行状态。
-e:若指令传回值不等于0,则立即退出shell。
-f:取消使用通配符。
-h:自动记录函数的所在位置。
-H Shell:可利用"!"加<指令编号>的方式来执行history中记录的指令。
-k:指令所给的参数都会被视为此指令的环境变量。
-l:记录for循环的变量名称。
-m:使用监视模式。
-n:只读取指令,而不实际执行。
-p:启动优先顺序模式。
-P:启动-P参数后,执行指令时,会以实际的文件或目录来取代符号连接。
-t:执行完随后的指令,即退出shell。
-u:当执行时使用到未定义过的变量,则显示错误信息。
-v:显示shell所读取的输入值。
-x:执行指令后,会先显示该指令及所下的参数。
set 11 22 33 44
echo $1 $2 $3 $4
echo "$@" #全部输出
echo "$#"  #代表元素个数
eval echo "$$#"
  • 声明类型变量
declare/typeset [-a/-i/-x/-r/-p/] 变量
-a 将变量定义成数组
-i 将变量定义成整数
-x 将变量定义成环境变量
-r 将变量定义成readonly
-p:显示变量定义的方式和值
+:取消变量属性,但是 +a 和 +r 无效,无法删除数组和只读属性,可以使用unset删除数组,但是 unset 不能删除只读变量。
  • local关键字,用来在作用域内创建变量,出了作用域被销毁;
  • exportshell变量或函数设置导出属性,成为环境变量,无法对未定义的函数添加导出属性。同时,重要的一点是,export的效力仅有效于该次登陆操作,注销或者重新开一个窗口,export命令给出环境变量都不存在了。通过./xxx.sh或者sh xxx.sh运行脚本,export声明的变量会失去效用,因为子shell执行完毕之后被销毁掉了。
-f:代表[变量名称]为函数名称。。
-n:删除变量的导出属性。变量实际上并未删除,只是不会输出到后续指令的执行环境中。
-p:显示全部拥有导出属性的变量。
-pf:显示全部拥有导出属性的函数。
-nf:删除函数的导出属性。
--:在它之后的选项无效。
  • 通配符
*:匹配任意字符串,包括空字符串,不包含对“/”字符的匹配。
?:匹配任意单个字符,不能匹配“/”字符。
[abc]:匹配“a”或者“b”或者“c”字符。
[^abc]:不匹配“a”或者“b”或者“c”字符。
[a-z]:匹配26个英文小写字符中任意一个。
  • expr:一款表达式计算工具,使用它完成表达式的求值操作。注意空格
!#/bin/bash
    declare -i a=2 b=3
    c=`expr $a + 1`
    d=`expr $b + 2`
    echo $(($c + $d))
  • eval:会对后面的commandLine进行两遍扫描,如果第一遍扫描后,commandLine是个普通命令,则执行此命令,如果commandLine中含有变量的间接引用,则保证间接引用的语义。

六、函数

function dowork {
}
或者
function dowork() {
}
或者
dowork() {
}

声明规则:

  • function,可以不写(),没有function,必须写();
  • 函数名和{之间必须有空格;
  • 不得声明形式参数;
  • 必须在调用函数的地方之前,声明函数;
  • 无法重载;
  • 后来声明的函数会覆盖之前的声明;
  • 没有返回值得函数,默认返回函数内最后一条指令的返回值,有返回值的函数,只能返回整数;
  • 需要获得函数值,只能通过$?获得,通过=获得是空值
  • 我们可以将shell中函数,看作是定义一个新的命令,因此各个输入参数直接用空格分隔。命令里面获得参数方法可以通过:$0...$n得到。$0代表函数本身。
1. $#:传入的参数的个数;
2. $*:所有的位置参数(作为单个字符串);
3. $@:所有的位置参数(每个都作为独立的字符串);
4. $?:当前shell进程中,上一个命令的返回值,如果上一个命令成功执行则$?的值为0,否则为其他非零值;cmd1&&cmd2,如果cmd1回传的值为0则执行cmd2。cmd1||cmd2,如果cmd1回传的值不为0则执行cmd2。
5. $$:当前shell进程的pid;
6. $!:后台运行的最后一个进程的pid;
7. $-:显示shell使用的当前选项;
8. $_:之前命令的最后一个参数;
9. local: 函数中声明的变量是全局变量,通过local可以将变量限定在函数体内。

七、条件控制与循环

case语句:除最后一个分支外(这个分支可以是普通分支,也可以是*)分支),其它的每个分支都必须以;;结尾,;;代表一个分支的结束,不写的话会有语法错误。最后一个分支可以写;;,也可以不写,因为无论如何,执行到 esac 都会结束整个 case in 语句。
语法:

    case $变量 in
    "第一个变量内容")
        程序
        ;;    #结束
    *)  # 用来托底,没有匹配到数据
        ;;
    esac

实践:

#!/bin/bash
TestShellScript() {
    case $1 in
    "0")
    echo "0"
    ;;
    "1")
    echo "1"
    ;;
    "2")
    echo "2"
    ;;
    *)
    echo "default"
    ;;
    esac
}
TestShellScript 3

输出为default。

if语句[]判断符号,两个等号和一个等号等效,中括号里面的每个组件都需要空格分隔,中括号的变量都使用双引号,常量使用单引号或者双引号。
语法:

一个条件判断:
  if [ condation ]; then
      成立
  else
      不成立
  fi

多条件判断:
  if [ condation ]; then
      成立
  elif [ condation ]; then
      成立
  else
      不成立
  fi

实践:

#!/bin/bash
TestShellScript() {
    if [ "$1" == '0' ]; then
    echo "0"
    elif [ $1 = "1" ]; then
    echo "1"
    else
    echo "other"
    fi
}
TestShellScript 1

输出为1。

一些判断命令:

1. test命令测试
    test n1 -eq n2:
    //整数比较大小
    -eq:相等
    -ne:不等
    -gt:大于
    -lt:小于
    -ge:大于等于
    -le:小于等于
2. 字符串判断
    -z string:判断string是否为0,为空,则为true。
    -n string:判断string是否非0,为空,则为false。
    string1 = string2:字符串是否相等,相等为true。
    string1 != string2:字符串是否不等,相等为false3. 多重条件判断
    -a:两个条件同时成立,为true。
    -o:两个条件任何一个成立,为true。
    !:反向。
4. 文件类型判断
    -e:文件名是否存在。
    -f:该文件名是否存在且是否为文件。
    -d:该名称是否存在且为目录。
    -L:该名称是否存在且是否为链接文件。
5. 文件权限检测
    -r:是否存在是否有可读权限。
    -w:是否存在是否有可写权限。
    -x:是否存在是否有可执行权限。
    -s:是否存在且为非空白文件。
6. 两文件比较
    -nt 文件1是否比文件2新。
    -ot 文件1是否比文件2旧。
    -ef 文件1和文件2是否为同一个文件。

循环语句:

条件成立,则进行循环
语法:

while [ condation ]  #判断条件
do                   #循环开始
    程序
done                 #循环结束

实践:

#!/bin/bash
TestShellScript() {
    a=0
    while [ "$a" -lt '10' ]
    do
    a=`expr $a + 1`
    echo $a
    done
}
TestShellScript

输出为:
1
2
3
4
5
6
7
8
9

当条件成立,就终止循环: 语法:

until [ condation ]  #判断条件
do                   #循环开始
    程序
done                 #循环结束

实践:

#!/bin/bash
TestShellScript() {
    a=0
    until [ "$a" -gt '10' ]
    do
        echo $a
        a=`expr $a + 1`
    done
}
TestShellScript

输出:
0
1
2
3
4
5
6
7
8
9
10

按照指定次数循环
语法:

for var in con1 con2 con3 ...
do
  程序
done

for (( 初始值; 限制值; 执行步长 ))
do
  程序
done

实践:

#!/bin/bash
TestShellScript() {
    for a in a b c d e f g h
    do
    echo "$a"
    done

    # for ((a=0;a<10;a++))
    # do
    # echo $a
    # done
}
TestShellScript

八、实战

  1. 需要实现的功能:在指定路径下查找指定关键字所在的指定的文件类型以及上下文。
sh find_api.sh -d xxx/xxx -k "xx" --keyword "xxx" (-s) (-f) (-l)
  1. 用到的命令
    find:从指定的起始目录开始,递归地搜索其各个子目录,查找满足寻找条件的文件并对之采取相关的操作。
    • 该命令提供的寻找条件可以是一个用逻辑运算符notandor组成的复合条件;
      1. or:逻辑或,在命令中用-o表示。该运算符表示只要所给的条件中有一个满足 时,寻找条件就算满足;
      2. and:逻辑与,在命令中用-a表示,是系统缺省的选项,表示只有当所给的条件都满足时,寻找条件才算满足;
      3. not:逻辑非,在命令中用!表示。该运算符表示查找不满足所给条件的文件。
    • 当使用很多的逻辑选项时,可以用括号把这些选项括起来。为了避免Shell本身对括号引起误解,在话号前需要加转义字符\来去除括号的意义;
    • -name '字串'查找文件名匹配所给字串的所有文件,字串内可用通配符 *?[ ];
    • -exec命令名称{}对符合条件的文件执行所给的命令,而不询问用户是否需要执行该命令。{}表示命令的参数即为所找到的文件;命令的末尾必须加上终结符,终结符有两个:";""+"结束。其中";"会对每一个find到的文件去执行一次cmd命令。而"+"find到的文件一次性执行完cmd命令。

grep:全称是Global Regular Expression Print,是一种强大的文本搜索工具,它能使用正则表达式搜索文本,并把匹配的行打印出来。

grep -E --extended-regexp #将范本样式为延伸的普通表示法来使用,意味着使用能使用扩展正则表达式。
  1. 思路:命令行向脚本传递参数,脚本分解命令,拿到参数,执行真正的命令。

第一步:通过shift进行偏移,while遍历循环,case根据不同的传值拿到各项参数,存储在前面定义的全局变量中

第二步:处理前面拿到的目标路径,以及关键词和查找的文件类型。

  1. 最终实现
#!/bin/bash

#定义全局变量
DIRECTORY_PATH="" #文件夹路径
KEYWORD="" #指定关键词
FILE_TYPE="" #指定文件类型

SOURCE_TYPE='*.h *.m *.mm *.c *.hpp *.cpp *.swift *.xcconfig'
FRAMEWORK_TYPE='*.framework *.o *.tbd' 
LIB_TYPE='*.a *.dylib'


#定义帮助文档
function Show_help {
    cat <<EOF

    find_api.sh --directory <dir> 在指定目录指定文件内搜索指定关键字。

                -d|--directory <dir> - 指定查找目录,默认当前所在目录
                -k|--keyword <word> - 查找关键字
                -s|--source  - 指定查找源码文件
                -f|--framework - 指定查找framework文件
                -l|--lib - 指定查找libs文件
                --help  - prints help screen

EOF
}


#遍历循环拿到参数
while [[ $# -gt 0 ]]; do
    case "$1" in
    -d|--directory)
    shift
    DIRECTORY_PATH="$1"
    shift
    ;;
    -k|--keyword)
    shift
    KEYWORD="$KEYWORD $1"
    shift
    ;;
    -s|--source)
    FILE_TYPE="${FILE_TYPE} ${SOURCE_TYPE}"
    shift
    ;;
    -f|--framework)
    FILE_TYPE="${FILE_TYPE} ${FRAMEWORK_TYPE}"
    shift
    ;;
    -l|--lib)
    FILE_TYPE="${FILE_TYPE} ${LIB_TYPE}"
    shift
    ;;
    --help)
    Show_help
    exit 0
    ;;
    *)
    echo "Unknown option: $1"
    exit 1
    esac
done

# string="1,2,3,4,5"
# arr=(${string//,/ })#将,替换为空格
# for a in ${arr[@]}
# do
# echo $a
# done





#文件目录或者文件名, 文件格式, 关键字
Find_Api() {

    #处理关键字 grep -E "k1|k2" "文件名"
    if [[ -n $KEYWORD ]]; then
        local key_word=""
        keywordArr=($KEYWORD)
        for k in ${keywordArr[@]}
        do
        if [[ ! -n $key_word ]]; then
        key_word=$k
        else
        key_word="$key_word|$k"
        fi
        done
    else
        echo "缺少关键词"
        exit -1
    fi

    # #如果文件目录或者文件存在,处理文件格式 find "文件目录/文件名" -name "n1" -o -name "n2"
    if [[ -d $DIRECTORY_PATH ]]; then #该名称是否存在且为目录
        local type_names=""
        if [[ ! -n $FILE_TYPE ]]; then
            FILE_TYPE="$SOURCE_TYPE $FRAMEWORK_TYPE $LIB_TYPE"
        fi
        # array=($FILE_TYPE) #将字符串转化为数组
        #或者
        read -r -a array <<< "$FILE_TYPE" #将变量FILE_TYPE读取进数组array中,-r屏蔽\,如果没有该选项,则\作为一个转义字符,有的话\就是个正常的字符。-a后跟一个变量,该变量会被认为是个数组,然后给其赋值,默认是以空格为分隔符。
        for type in "${array[@]}"
        do
        if [[ ! -n $type_names ]]; then
            type_names="-name $type"
        else
            type_names="$type_names -o -name $type"
        fi
        done
    elif [[ -f $DIRECTORY_PATH ]]; then #文件名是否存在且是否为文件
        if grep -E "$key_word" "$DIRECTORY_PATH" > /dev/null; then
            echo "在文件\033[37;32;4m${DIRECTORY_PATH}\033[39;49;0m中找到了(\033[37;31;4m${key_word}\033[39;49;0m)关键字!"
        fi
    else
        echo "文件目录或文件不存在"
        exit -1
    fi


    local files=$(eval find $DIRECTORY_PATH $type_names)
    if [[ ! -n $files ]]; then
        echo "没找到该类型的文件: $FILE_TYPE"
        exit -1
    fi


    local find_count=0
    local file_count=0
    while read file; #解决路径中带空格会被分割的问题
    do
    file_count=$[$file_count+1]
    if [[ -d $file ]]; then
        local name=`basename $file` #用于打印目录或者文件的基本名称。
        pushd $file > /dev/null
        exec_name="${name/.framework}"
        if [[ -e $exec_name ]]; then
            if nm -pa $exec_name | grep -E $key_word > /dev/null; then
                find_count=$[$find_count+1]
                echo "$find_count:在文件\033[37;32;4m${file}\033[39;49;0m中找到了(\033[37;31;4m${key_word}\033[39;49;0m)关键字!"
        fi
        fi
        popd > /dev/null
    else
        if grep -E "$key_word" "$file" > /dev/null; then
            find_count=$[$find_count+1]
            echo "$find_count:在文件\033[37;32;4m${file}\033[39;49;0m中找到了(\033[37;31;4m${key_word}\033[39;49;0m)关键字!"
        fi
    fi
    done <<< "$files"

    echo "共扫描(\033[37;31;4m${file_count}\033[39;49;0m)个文件,发现(\033[37;31;4m${find_count}\033[39;49;0m)个文件包含关键字(\033[37;31;4m${key_word}\033[39;49;0m)......"
}

Find_Api

九、 Xcode执行脚本的三种方式:

  1. 通过添加新的脚本target,和为target的Build Phases中添加run script不同的是,这种方式只会执行脚本,不会编译;

  1. 同上,使用External Build System;

git clone --depth 1 https://github.com/llvm/llvm-project.git"

  1. 与xcconfig搭配使用,通过设定不同的参数来执行不同的脚本。