文章

shell基础

shell基础

Shell 基础

什么是 Shell?

Shell 是一个连接用户和操作系统的应用程序,它提供了人机交互的界面(接口),用户通过这个界面访问操作系统内核的服务。Shell 脚本是一种为 Shell 编写的脚本程序,我们可以通过 Shell 脚本来进行系统管理,同时也可以通过它进行文件操作

数组

1
2
3
4
5
6
7
8
9
10
A="a b c def"   #$A  表示一个单一的字符串
A=(a b c def)    #$A  表示为数组。

A=(a b c def)    # 定义$A数组
${A[@]}${A[*]}     可得到 a b c def (全部元素)
${A[0]}     可得到 a (第一个数组元素)${A[1]} 则为第二个数组元素
${#A[@]}${#A[*]}     可得到 4 (全部数组数量)
${#A[0]}     可得到 1 (第一个数组元素(a)的长度)${#A[3]}     可得到 3 (第四个数组(def)的长度)
A[3]=xyz    将第4个数组重新定义为 xyz

案例 1:

1
2
3
4
5
6
#!/bin/bash
ip_list=(10.6.207.1 10.6.207.11)
for i in ${ip_list[@]}
do
    echo $i 
done

shell 函数

函数定义

Shell 函数定义的语法格式如下:

1
2
3
4
function name() {
  statements
  [return value]
}

对各个部分的说明:

  • function 是 Shell 中的关键字,专门用来定义函数;
  • name 是函数名;
  • statements 是函数要执行的代码,也就是一组语句;
  • return value 表示函数的返回值,其中 return 是 Shell 关键字,专门用在函数中返回一个值;这一部分可以写也可以不写。
  • { } 包围的部分称为函数体,调用一个函数,实际上就是执行函数体中的代码。

函数定义时也可以不写 function 关键字:

1
2
3
4
name() {
  statements
  [return value]
}

如果写了 function 关键字,也可以省略函数名后面的小括号:

1
2
3
4
function name {
  statements
  [return value]
}

函数参数:
Shell 函数在定义时不能指明参数,但是在调用时却可以传递参数,并且给它传递什么参数它就接收什么参数
定义一个函数,计算所有参数的和:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash
function getsum(){
    local sum=0

    for n in $@
    do
         ((sum+=n))
    done

    return $sum
}
getsum 10 20 55 15  #调用函数并传递参数
echo $?

运行结果:
100

$@ 表示函数的所有参数,$? 表示函数的退出状态(返回值)

参数输入判断

1 个字符串参数不能为空

  • [ -n "$1" ] $1 非空
  • [ -z "${key}" ] key 为空
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 字符串为空判断
function checkOpts() {
    local key=$1
    if [[ -z "${key}" ]] ; then # -z为空
        echo -e "\033[31mFATAL: key should not be empty! \033[0m"
        return 1
    fi
}

# 字符串不为空判断
#!/bin/bash
echo "Shell 传递参数实例!";
echo "执行的文件名:$0";
echo "第一个参数为:$1";
echo "第二个参数为:$2";
echo "第三个参数为:$3";

arg1=arg;
if [ -n "$1" ] # -n非空
then
    echo "第一个参数$1"
else
    echo "第一个参数为空"
fi

文件是否存在判断

  • -f 文件是否存在
1
2
3
4
5
f [ ! -f "${path}" ]; then
    pwd
    echo -e "\033[31mFATAL: ${pwd}/$path 文件不存在 \033[0m"
    return 0
  fi

2 个字符串判断

1
2
3
4
5
6
7
if [ "$applicationId" = "$MAIN_APP_ID" ]; then
    echo "即将启动xxx"
    adb:start:xxx
elif [ "$applicationId" = "$SECOND_APP_ID" ]; then
    echo "即将启动yyy"
    adb:start:yyy
fi

参数个数判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/bash

if [ $# -eq 0 ]; then
    echo "没有输入参数"
elif [ $# -eq 1 ]; then
    echo "输入了一个参数"
else
    echo "输入了多个参数"
fi


# $#表示参数的个数
if [ $# -lt 2 ]; then # 小于2个参数
  echo -e "\033[31mFATAL 请输入至少2个参数,参数1:应用包名,参数2:包路径 \033[0m"
  echo "当前参数 $*"
  return 0
fi

# 表示输入参数的个数。如果#等于 0,则表示没有输入参数;如果#等于 1,则表示只输入了一个参数;否则,表示输入了多个参数。根据#的值,脚本会输出不同的结果。使用这种方式,可以方便地判断输入参数的个数,并根据不同的情况执行不同的操作。

函数调用

不管是哪种形式,函数名字后面都不需要带括号。

  • 调用 Shell 函数时可以给它传递参数,也可以不传递。如果不传递参数,直接给出函数名字
  • 如果传递参数,那么多个参数之间以空格分隔
  • Shell 也不限制定义和调用的顺序,你可以将定义放在调用的前面,也可以反过来,将定义放在调用的后面

函数案例

1
2
3
4
5
6
7
8
function test() {
    local p0=${0}
    local p1=${1}
    local p2=${2}
    local p3=${3}
    echo "p0=$p0, p1=$p1, p2=$p2, p3=$p3"
}
test "1" "2" "3"

输出:p0=shell.sh, p1=1, p2=2, p3=3

Shell 脚本参数传递的 2 种方法

Shell 特殊参数解释

  1. $*
    传递给脚本或函数的所有参数,参数用双引号也会被拆分
  2. $@
    传递给脚本或函数的所有参数,参数用双引号也会被拆分
  3. "$*"
    将所有的参数作为一个整体,包括空格的,双引号的
  4. "$@"
    会将各个参数分开,双引号的作为一个整体,空格区分参数
1
2
3
4
5
echo "print each param from \"\$@\""
for var in "$@"
do
    echo $var
done

https://blog.csdn.net/beibei0921/article/details/45287855
$0 表示命令本身,
S1 表示第一个参数,
S2 表示第二个参数,
测试:

1
2
3
4
5
6
7
8
9
10
#!/bin/bash
echo $0    # 当前脚本的文件名(间接运行时还包括绝对路径)。
echo $n    # 传递给脚本或函数的参数。n 是一个数字,表示第几个参数。例如,第一个参数是 $1 。
echo $#    # 传递给脚本或函数的参数个数。
echo $*    # 传递给脚本或函数的所有参数。
echo $@    # 传递给脚本或函数的所有参数。被双引号 (" ") 包含时,与 $* 不同。
echo $?    # 上个命令的退出状态,或函数的返回值。
echo $$    # 当前 Shell 进程 ID。对于 Shell 脚本,就是这些脚本所在的进程 ID。
echo $_    # 上一个命令的最后一个参数
echo $!    # 后台运行的最后一个进程的 ID 号

./test.sh test test1 test2 test3 test4 输出:
image.png

  • $*$@ 都表示传递给函数或脚本的所有参数,不被双引号 (“”) 包含时,都以 “$1”“$2” … “$n” 的形式输出所有参数
  • 但是当它们被双引号 (“”) 包含时,"$*" 会将所有的参数作为一个整体,以 “$ 1 $2 … $n”的形式输出所有参数;"$@"会将各个参数分开,以 “$1”“$2” … “$n” 的形式输出所有参数。

方法 1:$0,$1,$2

采用 $0,$1,$2..等方式获取脚本命令行传入的参数,值得注意的是,$0获取到的是脚本路径以及脚本名,后面按顺序获取参数,当参数超过 10个时 (包括 10个),需要使用 ${10},${11}….才能获取到参数。
优点:获取参数更容易,执行脚本时需要的输入少
缺点:必须按照顺序输入参数,如果中间漏写则参数对应就会错误

方法 2:getopts

语法格式:getopts [option[:]] [DESCPRITION] VARIABLE

  • option:表示为某个脚本可以使用的选项
  • ”:”:如果某个选项(option)后面出现了冒号(”:”),则表示这个选项后面可以接参数(即一段描述信息 DESCPRITION);没有冒号表现该选项没有参数
  • VARIABLE:表示将某个选项保存在变量 VARIABLE中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/usr/bin/env bash
# -n 名称
# -a 作者
# -h 帮助
while getopts ":n:a:h" optname
do
    case "$optname" in
      "n")
        echo "get option -n,value is $OPTARG"
        ;;
      "q")
        echo "get option -a ,value is $OPTARG"
        ;;
      "h")
        echo "get option -h,eg:./test.sh -n 编程珠玑 -a 守望先生"
        ;;
      ":")
        echo "No argument value for option $OPTARG"
        ;;
      "?")
        echo "Unknown option $OPTARG"
        ;;
      *)
        echo "Unknown error while processing options"
        ;;
    esac
    #echo "option index is $OPTIND"
done
  • n后面有 :,表示该选项需要参数,而 h后面没有:,表示不需要参数
  • 最开始的一个冒号,表示出现错误时保持静默,并抑制正常的错误消息

测试:

$ ./test.sh -a No argument value for option a

$ ./test.sh -h get option -h,eg:./test.sh -n 编程珠玑 -a 守望先生

$ ./test.sh -n 编程珠玑 -a 守望先生 get option -a ,value is 守望先生

> /dev/null

> /dev/null: 这部分命令将命令的输出重定向到 /dev/null 文件,因此结果不会在终端中显示。它只是让执行命令的用户获得的返回值。如果包已安装,则该命令将返回 “0”,否则返回 “1”

adb pull /sdcard/demo.mp4

image.png

adb pull /sdcard/demo.mp4 > /dev/null

image.png

引入其他独立的.sh文件

source

1
2
export COMMON_PROFILE=$HOME/.sh/.common_profile.sh
[ -f $COMMON_PROFILE ] && source $COMMON_PROFILE # 存在COMMON_PROFILE文件就加载

其他

dirname获取脚本所在的文件夹

dirname 可以获取一个文件所在的路径,dirname 的用处是:输出已经去除了尾部的 “/”字符部分的名称;如果名称中不包含 “/”,

则显示 “.”(表示当前目录)。

![image.png400](undefined)

直接从 dirname 返回的未必是绝对路径,取决于提供给 dirname 的参数是否是绝对路径

cdpwd 命令配合获取脚本所在绝对路径,正确的写法是这样的,

1
2
3
4
5
6
7
# 获取脚本所在的文件夹
SHELL_FOLDER=$(cd "$(dirname "$0")";pwd)

# 其他类似的写法1
CUR_DIR=$(cd `dirname $0` && pwd -P)
# 类似写法2
BASEDIR=$(dirname "$0")

shell脚本编写规范

指定解释器

1
#!/bin/bash 

解释器有很多种,除了 bash之外,我们可以用下面的命令查看本机支持的解释器:

1
2
3
4
5
6
7
8
9
10
11
12
13
cat /etc/shells
# List of acceptable shells for chpass(1).
# Ftpd will not allow users to connect who are not using
# one of these shells.

/bin/bash
/bin/csh
/bin/dash
/bin/ksh
/bin/sh
/bin/tcsh
/bin/zsh
/usr/local/bin/zsh

shell脚本加颜色,背景色

https://misc.flogisoft.com/bash/tip_colors_and_formatting

巧用 main函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env zsh
function func1(){
    #do sth
    echo func1
}
function func2(){
    #do sth
     echo func2
}
function main(){
    echo 'main'
    func1
    func2
}
main "$@" # "$@"把所有参数都传递给main函数

实现 main函数,使得脚本的结构化程度更好。

作用域

shell中默认的变量作用域都是全局的,比如下面的脚本:

1
2
3
4
5
6
7
8
#!/usr/bin/env bash
var=1
function func(){
    var=2
}
func
echo $var 
# 输出结果就是2而不是1

学会查路径

很多情况下,我们会先获取当前脚本的路径,然后一这个路径为基准,去找其他的路径。通常我们是直接用 pwd以期获得脚本的路径。
不过其实这样是不严谨的,pwd获得的是当前 shell的执行路径,而不是当前脚本的执行路径。
正确的做法应该是下面这两种:

1
2
script_dir=$(cd $(dirname $0) && pwd)
script_dir=$(dirname $(readlink -f $0 )) 

应当先 cd进当前脚本的目录然后再 pwd,或者直接读取当前脚本的所在路径。

使用新写法

  1. 尽量使用 func(){} 来定义函数,而不是 func{}
  2. 尽量使用 [[]] 来代替 []
  3. 尽量使用 $() 将命令的结果赋给变量,而不是 ` 反引号```
  4. 在复杂的场景下尽量使用 printf 代替 echo 进行回显
  • 路径尽量保持绝对路径,绝多路径不容易出错,如果非要用相对路径,最好用./修饰
  • 优先使用 bash 的变量替换代替 awk sed,这样更加简短
  • 简单的 if 尽量使用&& ,写成单行。比如 [[ x > 2]] && echo x
  • 当 export 变量时,尽量加上子脚本的 namespace,保证变量不冲突
  • 会使用 trap 捕获信号,并在接受到终止信号时执行一些收尾工作
  • 使用 mktemp 生成临时文件或文件夹
  • 利用/dev/null 过滤不友好的输出信息
  • 会利用命令的返回值判断命令的执行情况
  • 使用文件前要判断文件是否存在,否则做好异常处理
  • 不要处理 ls 后的数据 (比如 ls -l | awk '{ print $8 }'),ls 的结果非常不确定,并且平台有关
  • 读取文件时不要使用 for loop 而要使用 while read

静态检查工具 shellcheck

Ref

Ref

本文由作者按照 CC BY 4.0 进行授权