0%

shell技巧介绍

Shell 脚本在我们日常开发和学习都有举足轻重的地位,比如看一些开源项目,比如项目中的各式各样的脚本,对于促进生产力工具有很大帮助!而且shell最大的好处就是非常的方便!bash是一个非常成熟的脚本语言,基本上具有现代脚本语言的所有特性,所以你可以通过bash实现各种各样的脚本!

我们日常开发中使用mac作为开发机的话需要考虑和linux命令兼容的问题,所以需要在mac上替换成gun相关的命令!

1、命令小技巧

1、-x 命令进行跟踪调试执行

注意set -x是开启trace,set +x是关闭trace

1
2
3
4
5
6
7
8
9
#!/bin/sh

num1=10
num2=20
if (($num1 <= $num2)); then
echo num1 lesser equal num2
else
echo num1 greater num2
fi

执行:

1
2
3
4
5
6
➜  note git:(master) ✗ sh -x /Users/fanhaodong/Desktop/project/test.sh
+ num1=10
+ num2=20
+ (( 10 <= 20 ))
+ echo num1 lesser equal num2
num1 lesser equal num2

2、-c 命令 (执行命令参数)

1
2
3
4
5
6
7
➜  note git:(master) ✗ sh -c << EOF "
dquote> echo hello world
dquote> echo hello world2
dquote> "
heredoc> EOF
hello world
hello world2
  1. 这种经常会在网上下载一个脚本然后直接执行,可以 sh -c "curl xxxx.sh"

3、使用set变量

一般就是 set -ex ,或者执行的时候 bash -ex

1
2
3
4
5
6
7
8
9
10
#!/bin/sh

# -v Print shell input lines as they are read.
# -x Print commands and their arguments as they are executed.
# -e Exit immediately if a command exits with a non-zero status.
set -ex

echo "hello world"

exit 1

执行

1
2
3
4
5
6
➜  makefile git:(master) ✗ sh ./main.sh
+ echo 'hello world'
hello world
+ exit 1
➜ makefile git:(master) ✗ echo $?
1

帮助可以看: sh -c "help set"

2、语法小技巧

1、引用变量

一般推荐正确用法是,变量使用 "" 双引号引用,其次就是变量使用 ${}进行引用!very good!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 使用 ${} 引用变量,拒绝歧义
➜ ~ data="hello" ;echo $dataa

➜ ~ data="hello" ;echo ${data}a
helloa

# 字符串用 "" 引用
➜ ~ data="hello " ;echo "${data}a"
hello a
➜ ~ data=hello ;echo "${data}a"
helloa

# 单引号不会进行变量赋值 (注意)
➜ ~ data=hello ;echo '${data}a'
${data}a

2、 $( cmd ) 和 `cmd` 执行命令

1
2
3
4
➜  ~ echo $(uname)
Darwin
➜ ~ echo `uname`
Darwin

3、 cat [>>|>] [file] << EOF .... EOF 写入文件

如果重定向的操作符是<<-,那么分界符(EOF)所在行的开头部分的制表符(Tab)都将被去除。这可以解决由于脚本中的自然缩进产生的制表符。

1
2
3
4
5
6
7
8
9
10
11
12
➜  test cat > glide.yaml << EOF
heredoc> name: tom
heredoc> age: 10
heredoc> hobby:
heredoc> - football
heredoc> EOF

➜ test cat glide.yaml
name: tom
age: 10
hobby:
- football

4、管道符 和 xargs

1. 管道符

管道符作用就是把上一个命令的标准输出作为下一个命令个标准输入

1
2
➜  echo "hello wrold" | python -c 'import sys; print(sys.stdin.read())'
hello wrold

2. xargs

1 .xargs的作用就是把上个命令的标准输出 作为 下一个命令的参数

1
2
➜  echo "hello wrold" | xargs python -c 'import sys; print(sys.argv)'
['-c', 'hello', 'wrold']
  1. 如果想替换分隔符号需要输入参数 -d, 同时你想打印所执行的命令 -t
1
2
3
4
5
6
7
➜  ~ echo "hello:wrold" | xargs -t -d ':' python -c 'import sys; print(sys.argv)'
python -c 'import sys; print(sys.argv)' hello 'wrold'$'\n'
['-c', 'hello', 'wrold\n']

# 上面带\n的原因是echo默认换行,需要
➜ ~ echo -e "hello:wrold\c" | xargs -d ':' python -c 'import sys; print(sys.argv)'
['-c', 'hello', 'wrold']
  1. xargs 是并行执行的,可以通过如下测试**, -P等于0表示不限制进程数**,默认是1, -n等于1表示参数按每一个进行拆分

​ 这个命令相当棒,就是可以并行执行!

1
2
3
4
5
➜  echo "hello world" | xargs -P0 -n1  sh -c 'echo "start $$"; sleep 5s; echo "end $$"'
start 21377
start 21378
end 21377
end 21378
  1. -I,可以替换命令行参数
1
2
3
4
5
➜  ~ echo "hello wrold" | xargs echo
hello wrold

➜ ~ echo "hello wrold" | xargs -I {} echo {}
hello wrold

5、特殊变量

  • $0: 当前脚本的文件名
  • $[n] : 传递给脚本或函数的参数。n 是一个数字,表示第几个参数。例如,第一个参数是$1,第二个参数是$2。
  • $#: 传递给脚本或函数的参数个数。
  • $*: 传递给脚本或函数的所有参数
  • $@: **传递给脚本或函数的所有参数(推荐使用这个)**,当使用 "" 双引号引用是 $*会变成字符串而不是数组
  • $?: 上个命令的退出状态,或函数的返回值。一般情况下,大部分命令执行成功会返回 0,失败返回 1。
  • $$: 当前Shell进程ID。对于 Shell 脚本,就是这些脚本所在的进程ID。
  • $! 子进程ID

6、[[]][] 标准 以及基本语法规范

1
2
3
4
5
6
7
8
9
10
11
12
name="xiao_li"

# 比较 = / !=
if [ "$name" = "xiao_li" ]; then echo "1(bash运算符). name is xiao_li"; fi
if [[ "$name" = "xiao_li" ]]; then echo "1(bash运算符). name is xiao_li"; fi

age=10
if [ "$name" = "xiao_li" ] && [ "$age" -eq 10 ]; then echo "3(sh逻辑逻辑). name is xiao_li and age is 10"; fi
if [[ "$name" = "xiao_li" && "$age" -eq 10 ]]; then echo "4(bash逻辑逻辑). name is xiao_li and age is 10"; fi

if [[ "$name" == xiao_* ]]; then echo "5(bash通配符). xiao_li has prefix xiao_"; fi
if [[ "$name" =~ ^xiao_[a-z]+* ]]; then echo "6(bash正则). xiao_li reg match ^xiao_[a-z]+* "; fi

总结: 大部分情况下 [] 都可以满足需求,如果使用到通配符、正则等需求再使用[[]],其次关于逻辑运算推荐使用 [] && [] 这种方式,除非有复杂的逻辑运算需求!

7、/bin/sh 与 /bin/bash 的区别

/bin/sh 与 /bin/bash 的区别

3、获取命令结果 $(cmd)

有两种写法,一种是 $()这个并不是所有的shell都支持,但是比较直观, 另外一种是 "``" (它可是适用更多的平台)

1
2
3
4
5
#!/bin/sh

echo `ls -a /Users/fanhaodong/note`

echo $(ls -a /Users/fanhaodong/note)

输出:

1
2
. .. .DS_Store 1714.jpg docker-rocketmq-cluster gridea-home hexo-home note pdf vuepress-starter
. .. .DS_Store 1714.jpg docker-rocketmq-cluster gridea-home hexo-home note pdf vuepress-starter

4、输入输出重定向 2>&1

使用

程序中经常有,标准输出,但是还有错误输出,因此需要合并到一个流中

其实Go的程序中正执行脚本的时候可以指定,标准输出和错误输出

1
2
3
command := exec.Command(shell, "-c", cmd)
command.Stdout = os.Stdout
command.Stderr = os.Stderr

使用的时候:

  • 默认为标准输出重定向,与 1> 相同
  • 2>&1 意思是把 标准错误输出 重定向到 标准输出.
  • &>file 意思是把标准输出和标准错误输出 都重定向到文件file中

例如:

command >out.file 2>&1 &

command >out.file是将command的标准输出重定向到out.file文件,即输出内容不打印到屏幕上,而是输出到out.file文件中。2>&1 是将标准出错重定向到标准输出,这里的标准输出已经重定向到了out.file文件,即将标准出错也输出到out.file文件中。最后一个&,是让该命令在后台执行。

参考

https://www.cnblogs.com/caolisong/archive/2007/04/25/726896.html

5、If语句

​ if 其实就是test 命令

1、格式

  1. 换行写
1
2
3
4
5
6
7
if [ condition ]; then
# body
elif [ condition ]; then
# body
else
# body
fi

2)非换行写

1
if [ -f "/Users/fanhaodong/note/note/Makefile1" ]; then  echo 111 ; echo 222 ;elif [ -f "/Users/fanhaodong/note/note/README.md" ]; then  echo 333 ; echo 4444 ; else  echo 555 ; echo 666 ; fi

2、结果获取/判断

结果输出0 ,表示为真,可以通过$? 来获取结果

3、例如调试条件

1
2
3
➜  note git:(master) ✗ test "abc"!="def"
➜ note git:(master) ✗ echo $?
0

4、测试文件是否存在

  • 如果你要判断一个文件是否存在,只需要 -e 即可,输出0 表示文件存在 (不在判断类型的时候推荐使用这个)
  • 如果你要判断一个文件是否为文件夹,并且判断是否存在,只需要 -d 即可
  • 如果你要判断一个文件是否为常规文件 ,并且判断是否存在,只需要-f 即可
  • -L filename 如果 filename为符号链接,则为真
1
2
3
4
5
[root@019066c0cd63 ~]# ls -al
lrwxrwxrwx 1 root root 5 Mar 1 09:49 c.txt -> a.txt
[root@019066c0cd63 ~]# [ -L "./c.txt" ]
[root@019066c0cd63 ~]# echo $?
0
  • -r filename 如果 filename可读,则为真
  • -w filename 如果 filename可写,则为真
  • -x filename 如果 filename可执行,则为真
  • -s filename 如果文件长度不为0,则为真
  • -h filename 如果文件是软链接,则为真
1
2
3
4
5
6
➜  note git:(master) ✗ [ -f "/Users/fanhaodong/note/note/Makefile" ]
➜ note git:(master) ✗ echo $?
0
➜ note git:(master) ✗ [ -f "/Users/fanhaodong/note/note/Makefile1" ]
➜ note git:(master) ✗ echo $?
1

5、字符串操作

​ 字符串推荐加 "" 进行定义

  1. 判断字符串是否为空 -z (zero)么
1
2
3
4
5
6
7
#!/bin/sh

str=""
if [ -z "${str}" ]; then
echo str is empty
fi
# str is empty

2)判断两个字符串是否相同

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/sh

str1="str"
str2="str2"

if [ "$str1" = "$str2" ]; then
echo str1 is equal str2
else
echo str1 is not equal str2
fi

# str1 is not equal str2

4、测试一个命令是否存在 command -v $#

1
2
3
4
5
6
7
8
#!/bin/sh
cmd=go
if [ `command -v $cmd` ]; then
echo $cmd command is exists
else
echo $cmd command not exists
fi
# go command is exists

5、获取字符串长度 ${#var}

1
2
3
4
5
6
7
8
#!/bin/sh
str="hello "
str1=hello
echo str 的长度是 ${#str}
echo str1 的长度是 ${#str1}

#str 的长度是 8
#str1 的长度是 5

6、数字比较

  • -eq 等于
  • -ne 不等于
  • -gt 大于
  • -ge 大于等于
  • -lt 小于
  • -le 小于等于
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/sh

num1=10
num2=20
if (($num1 <= $num2)); then
echo num1 lesser equal num2
fi

if [ $num1 -le $num2 ]; then
echo num1 lesser equal num2
fi

# num1 lesser equal num2
# num1 lesser equal num2

7、shell脚本中if判断’-a’ - ‘-z’含义

https://blog.csdn.net/tootsy_you/article/details/95597376

image-20210527103147909

image-20210527103211720

image-20210527103224576

6、数组和循环

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#!/bin/bash

#### 数组

# 1. 定义数组
arr=("00" "11" "22" "33" "44")

# 2. 遍历数组
for elem in "${arr[@]}"; do
echo "elem: ${elem}"
done

# 数组在shell中定义和map差不多,map也可以使用数组进行表达,key=index, value=elem
for index in "${!arr[@]}"; do
echo "elem: ${arr[index]}"
done

# 标准for循环
for ((index = 0; index < ${#arr[@]}; index++)); do
echo "elem: ${arr[index]}"
done

# 3. 添加元素
arr+=("55" "66")

echo "len(arr) = ${#arr[@]}"

# 4. 元素切片
echo "arr[2:4] = ${arr[*]:2:2}"

# 5. 判断元素是否存在
# $1=want
# $2...=arr
# 注意shell函数参数没有数组的概念,只能说你把它展开成多个参数
# 注意 $@ 为参数, $# 为参数长度
function contains() {
# $0 为执行文件
# $1 ... 为参数
want="$1"
for elem in "${@:2}"; do
if [ "$elem" == "$want" ]; then return 0; fi
done
return 1
}

if contains "111" "${arr[@]}"; then
echo "contains 111"
else
echo "not contains 111"
fi

if contains "11" "${arr[@]}"; then
echo "contains 11"
else
echo "not contains 11"
fi

7、switch

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

name="$1"

case "$name" in
"11 222"| "222 333")
echo "num is: 11 222| 222 3333"
;;
44)
echo "name is: 44"
;;
*)
echo "num is(*): $name"
;;
esac

输出:

1
2
3
4
5
6
➜  script git:(master) ✗ bash -e string.sh "11 222" 
num is: 11 222| 222 3333
➜ script git:(master) ✗ bash -e string.sh "11 222 3333"
num is(*): 11 222 3333
➜ script git:(master) ✗ bash -e string.sh "44"
name is: 44

字符串操作

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

name="xiao_min"

# 移除前缀
suffix=${name#"xiao_"}
echo "suffix: $suffix" # suffix: min

# 移除后缀
prefix=${name%"$suffix"}
echo "prefix: $prefix" # prefix: xiao_

# 字符串替换
name2=${name/"min"/"li"}
echo "name2: $name2" # name2: xiao_li

getopt

实际中我们写一些复杂的脚本(命令行工具)都需要解析参数,shell提供了getoptsgetopt 命令可以帮助实现命令行解析

1
2
3
4
~ type getopts
getopts is a shell builtin
~ type getopt
getopt is /usr/bin/getopt
  1. -o 或者 --option 表示短选项

  2. -l 或者--longoptions 表示长选项,多个参数用 , 进行分割

  3. 选项会有 必须可选Flag 三种状态,是通过申明 :来表示

    1. required 必须:参数定义后面需要跟: , 必须的意思就是我定义 required 的参数那么这个参数后面一定得跟东西,不是必须传递这个参数,例如
    1
    2
    3
    4
    5
    6
    7
    8
    9
    ➜  ~ getopt -o 'a:' --  -a
    getopt: option requires an argument -- a
    --

    ➜ ~ getopt -o 'a:' -- -a 1
    -a '1' --

    ➜ ~ getopt -o 'a:' -- 111 222
    -- '111' '222'
    1. optional 可选:参数定义后面跟:: , 解释同上面,optional 参数就是你后面可以不跟东西,但是参数值需要附加在值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    ➜  ~ getopt -o 'a::' --  -a 1
    -a '' -- '1'
    ➜ ~ getopt -o 'a::' -- -a1
    -a '1' --
    ➜ ~ getopt -o 'a::' -- -a
    -a '' --

    ➜ ~ getopt -o '' -l 'name::' -- --name
    --name '' --
    ➜ ~ getopt -o '' -l 'name::' -- --name='111'
    --name '111' --
    ➜ ~ getopt -o '' -l 'name::' -- --name
    --name '' --
    1. flag : 参数后面啥也不跟

    2. 例如 -o 'abc:d::e' 表示 a、b不接收参数,c必须要跟一个参数,d为可选参数,e不接收参数

  4. 注意:optioanl 参数值赋值有点奇葩,这个也是必然的不然无法区分出来optioanl 和 required,例如 短选项需要直接附加在值后面,长选项需要用=来附加值

getopts

getopts 是shell的内置方法,但是不能处理长命令,用起来会比较方便和简单,所以经常用于处理一些简单场景!

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
#!/bin/bash

# getopts ":ab:c:" opt # 其中最前面的:表示忽略错误
# OPTIND: 选项索引 option index, 下一个要处理的元素位置, 初始化值是1, 大部分场景也不会用到
# OPTARG: 选项参数 option arg

echo "args: $*"
echo "OPTIND=[$OPTIND]"

while getopts ":ab:c:" opt; do
case $opt in
a)
echo "this is -a option. OPTARG=[$OPTARG] OPTIND=[$OPTIND]"
;;
b)
echo "this is -b option. OPTARG=[$OPTARG] OPTIND=[$OPTIND]"
;;
c)
echo "this is -c option. OPTARG=[$OPTARG] OPTIND=[$OPTIND]"
;;
*)
echo "there is unrecognized parameter."
exit 1
;;
esac
done

执行:

1
2
3
4
5
6
7
➜  vscode git:(master) ✗ bash run.sh -a -b1 -c 1 -c2   
args: -a -b1 -c 1 -c2
OPTIND=[1]
this is -a option. OPTARG=[] OPTIND=[2]
this is -b option. OPTARG=[1] OPTIND=[3]
this is -c option. OPTARG=[1] OPTIND=[5]
this is -c option. OPTARG=[2] OPTIND=[6]

getopt 命令

mac的话需要: brew install gnu-getopt

帮助文档:https://linux.die.net/man/1/getopt

getopt既然是命令,说白了,我们可以怎么处理呢?很简单

1
2
3
4
5
6
7
8
# a为flag
# b为required 如果需要带`-d` 参数那么后面一定要附加值
# c为optional
# help 为flag
# size为optionl
# name为required
➜ ~ getopt -o 'ab:c::d' -l 'help,size::,name:' -- -a --name '1' -b '1111' -c1111 --size='1111'
-a --name '1' -b '1111' -c '1111' --size '1111' --

现代编程语言 - bash shell

bash 和 sh的区别

bash shell 是一门现代的编程语言

sh 仅支持一些简单的逻辑判断、循环等,并不支持一些丰富的内置能力,比如数组、数学计算、条件判断支持正则/通配符等一些高级特性!

控制逻辑

条件控制

1
2
3
4
5
6
7
8
9
10
11
12
name="xiao_li"

# 比较 = / !=
if [ "$name" = "xiao_li" ]; then echo "1(bash运算符). name is xiao_li"; fi
if [[ "$name" = "xiao_li" ]]; then echo "1(bash运算符). name is xiao_li"; fi

age=10
if [ "$name" = "xiao_li" ] && [ "$age" -eq 10 ]; then echo "3(sh逻辑逻辑). name is xiao_li and age is 10"; fi
if [[ "$name" = "xiao_li" && "$age" -eq 10 ]]; then echo "4(bash逻辑逻辑). name is xiao_li and age is 10"; fi

if [[ "$name" == xiao_* ]]; then echo "5(bash通配符). xiao_li has prefix xiao_"; fi
if [[ "$name" =~ ^xiao_[a-z]+* ]]; then echo "6(bash正则). xiao_li reg match ^xiao_[a-z]+* "; fi

循环语句

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#!/bin/bash

# 以下全部代码仅支持在bash中使用!

# {1..5}迭代器生成[1-5]的数组
for elem in {1..5}; do
echo "[for-range] elem: $elem"
done

arr=(1 2 3)

arr+=(4) # append

arr[0]=11 # set
echo "arr[0] = ${arr[0]}" # get

# arr - size
length=${#arr[@]}
echo "arr length = ${length}"

# foreach
for elem in "${arr[@]}"; do echo "[foreach] elem: $elem"; done

# for index (需要bash>=4.0, 支持 associative arrays)
for index in "${!arr[@]}"; do echo "[for-index] index: $index, value: ${arr[$index]}"; done

# for
for ((x = 0; x < length; x++)); do
echo "[for] index: $x, value: ${arr[$x]}"
done

# while
index=0
while [ $index -lt "$length" ]; do
echo "[while] index: $index, value: ${arr[$index]}"
index=$((index + 1))
done

# break
for elem in "${arr[@]}"; do
if [ "$elem" -gt 10 ]; then
echo "[break] $elem > 10"
break
fi
done

数据结构

本人坚持原创技术分享,如果你觉得文章对您有用,请随意打赏! 如果有需要咨询的请发送到我的邮箱!