1 Star 0 Fork 11

coder_lw / wiki

forked from deepinwiki / wiki 
加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
shell脚本.md 22.16 KB
一键复制 编辑 原始数据 按行查看 历史
htqx 提交于 2023-07-23 13:25 . 行列转换

SHELL脚本

前言

管理系统,难免要用到一些脚本,因为很多命令要输入很多遍,难免会记不起那么多,如果每次要完成某样任务,都要重新写一遍命令,那是非常痛苦的事情。可以把shell脚本,看作是一份管理文档,它记录你完成某样任务需要执行的命令步骤。当然,这份文档,还有自动运行的效果。

本文主要围绕 linux 中的常见的 bash shell 展开。

基本概念

脚本语言和一般的编程语言比起来,要简化很多东西,因为它是关注特定领域的特殊语言,它就是为了把系统管理员日常输入的命令给统一化,程序化而已。因此它的基础,首先是众多的linux命令,然后才是脚本语言特有的一些简单的语法结构。

基本技巧

  1. 按 tab 补全
    1. bash-completion(arch 包):可补充参数的提示
  2. 按上下箭头,可以获取上一条和下一条历史命令
    1. ctrl + p : 上
    2. ctrl + n : 下
    3. ctrl + b : 左
    4. ctrl + f : 右
    5. ctrl + a : 行首
    6. ctrl + e : 行尾
    7. alt + b : 上一个单词
    8. alt + f : 下一个单词
    9. ctrl + k : 从当前位置开始剪切,直到行尾
    10. ctrl + u: 从当前位置开始剪切,直到行首
    11. ctrl + y : 粘贴
    12. alt + l : 单词转换为小写
    13. alt + u : 单词转换为大写
  3. 控制当前运行的程序
    1. ctrl + c : 关闭
    2. ctrl + d : 中止输入
    3. ctrl + z : 后台执行,fg 命令可以恢复到前台
  4. 历史命令(bang bang)
    1. !! : 执行上一条命令
    2. !str : 匹配 str 的上一条命令
    3. !N : 第 n 条命令
    4. !-N : 上 n 条命令
    5. !$ : 上一条命令最后的参数
    6. !* : 上一条命令的所有参数
    7. !!:p : 回显(但不执行)上一条命令
    8. !!:1 : 上一条命令的第一个参数
    9. !!:s^old^new : 执行上一条命令,但先替换掉 old 为 new
    10. ctrl + r : 在历史中搜索匹配的命令
  5. 改变目录
    1. cd - : 上一个目录
    2. cd ~ : 用户家(home)目录
    3. pushd /: 存档当前目录,并跳转到指定目录
    4. popd : 恢复存档中的目录
  6. 管道(操控标准输入 /dev/stdin ,标准输出 /dev/stdout, 标准错误输出 /dev/stderr )
    1. a | b : a 命令的输出是 b 命令的输入
    2. a > file : a 命令的输出导出到 file 文件
    3. a >&0 : a 命令的输出导出到标准输入(0 标准输入,1 标准输出,2 标准错误)
    4. a 2>&0 : a 命令的错误导出到标准输入
    5. a >&0; b <&0 : 约等于 a | b
    6. a | tee /dev/tty | b : tee 将 a 的输出保存到 file 文件,然后转发给 b
    7. a | xargs b : a 的输出作为参数传递给 b 命令
    8. /dev/tty: 当前终端
    9. 管道中的命令在另一个进程执行,因此存在通信难度,当前脚本(父)可以传递变量给管道中的命令(子),但它无法反向传递
      1. mkfifo:命名管道,子进程持续写入,父脚本读取即可。
      2. mkfifo pipe: 父创建
      3. echo "xxx" > pipe &:子后台写入
      4. read var < pipe: 父读取
  7. 模式扩展(globbing)
    1. ~:主目录,即/home/用户名
    2. ~+: 当前目录
    3. ?: 匹配一个任意字符,如 ?.sh 匹配 a.sh, b.sh 等目录存在的文件
    4. *: 匹配任意个任意字符,0-n
    5. [ab]: 匹配 a 或者 b,即中括号指定字符集
    6. [!ab]:上面的反义,除了在字符集中出现的字符都匹配
    7. [a-z]: 匹配 a 到 z 的所有字符,包括 az
    8. [a-z]*: 组合,表示匹配任意字符集中的 n 字符
    9. w{a,b,c}: 得到 wa wb wc 序列,必须至少一个逗号
    10. {0..100}: 得到 0 1 2 ... 100 的序列
    11. {00..99..2}: 得到 00 02 ... 98 的序列,步长 2

语法结构

基本要素

  1. 脚本的每一行实质上就是一条命令。
  2. 大小写是有区别的。
  3. 有无空格是有差别的。
  4. #代表注释,程序会忽略。
  5. ""双引号内是字符串,但是会被shell解析内部的$符号
  6. ''单引号内是字符串,且不会被解析
  7. 空值:它不等于空字符串,最好替换为空字符串

变量

hi=x; # hi 是变量,x是值,变量能存储值。
hello=$hi; # $hi ${hi} 这两种写法都是获取变量的值
# 变量可以不先设置值,默认为空
hello=${hi} #同上

#特别注意
# shell变量是一种简单文字替换的规则,如:
a='x y';
b=$a; #$a被简单替换成b='x y'
unset a;#取消a变量的设置
b=$a; #等价 b= ,这个奇怪的结果在有些就会出现问题,因此最好用"$a"来取代$a,这样就算$a等于空,式子仍然等于b="",没那么怪异。
# 因此很多时候需要把变量放进双引号内
b="$a" # 建议这种写法

数组

a=(1 2 3 4) # 创建数组
b=${a[0]} # 获取数组第一个元素
b=$[a[0]] # 结果同上
let b=a[0] # 结果同上
a=(*.pdf) # 当前目录下所有 pdf 文件组成的数组

echo ${a[@]} # 返回所有成员,注意成员有空格时要放入字符串中
echo ${a[*]} # 返回一个包含所有成员的字符串
b=("${a[@]}") # 拷贝数组

for i in "${a[@]}";do echo $i; done # 枚举数组
echo ${#a[@]} # 数组长度
echo ${!a[@]} # 数组下标
a+=(5) # 添加成员

表达式

let hi=1+1; #有返回结果的式子叫表达式,如后半部分hi=1+1等于2. let hi=1+1这个式子整体是个语句,它表示自动计算表达式部分,因而hi=2。
a=1+1;  # 没有let的结果是把1+1当作字符串来处理,而不是表达式,因而echo $a 输出 1+1 ,千万要注意shell这个特点,一定要用本节表达式的固有语法来写表达式
a=$((hi+=1)); #$双括号内是表达式.
a=$[1+1]; #同上
a=$(ls); #$单括号内是命令,取得命令的输出结果
# 加减乘除、取余( + - * / % )的结果是整数
# 在括号内的表达式,比外面书写要自由一些,可以添加空格

规范写法说明:

# 字符串
a=1 # 注意不要多余空格
a="a b" # 用字符串包裹,因为可能有空格
b="$a"

# 表达式
a=1
let c=a+1 
c=$[a+1]

# 奇怪的技术
a=1
b=a # b='a'
let c=b # c=1 会递归的计算出结果
d=$b # d='a' 保留原样
e=$[b] # e=1

浮点小数:

# shell 只支持整数,但是可以通过bc命令来计算浮点
let a=$(echo "scale=2;5*9.9/3.14"|bc) #scale表示除法计算保留2位小数。
# 支持运算 + - * / % ^幂 sqrt()开平方
# 支持逻辑 > < >= <= == != && || ! 
# 支持 read()读取 length()有效长度 scale()精度 
# bc -l 扩展以下运算
# s(弧度) 正弦
# c(弧度) 余弦
# a(弧度) 反正切
# l() 自然对数
# e() 指数函数
# j() n阶
# 可以自定义一个函数来使用bc
fbc(){ echo "scale=2;$1"|bc -l; }
fbc "3*5.7^3*sqrt(3+69%10)/3.14"

判断表达式

[ -d x ]  #注意中括号内侧都要有空格,表示x是空字符串就返回0,而0在shell中代表true,即为真。
# 判断表达式是程序智能的基础,它能根据不同情况,配合流程语句选择不同的执行路线。
[[ -d x ]] # 双中括号比单的更通用
关键字 分类 意义
-d 文件 是否目录
-e 文件 文件、目录、设备等是否存在
-f 文件 文件
-p 文件 命名管道
-L 文件 链接
-r 文件属性
-w 文件属性
-x 文件属性 执行
-nt 修改时间 [ a -nt b ] a比b新
-ot 修改时间 [ a -ot b ] a比b旧
-z 字符串
-n 字符串 非空
== 字符串 a == b 相等
!= 字符串 a != b 不等
-eq 数值 相当于 ==
-ne 数值 !=
-ge 数值 >
-gt 数值 >=
-le 数值 <
-lt 数值 <=
  1. >、<这些符号不能直接使用,因为shell会把它视为管道符,而不是大于小于。并且也不支持>= <=
  2. 注意关键字两边要有空格,不能连在一起,否则就变成一串字符串了

将几个判断组合起来的表达式,称为逻辑表达式:

逻辑表达式 例子 意义
&& [ -r a ] && [ -w a ] a可读且可写
! ! [ -f a ] a不是文件
|| [ -f a ] || [ -d a ] a是文件或目录
  1. 可以写成[[ -fa || -d a ]] 双中括号形式,更加智能好用。

常用语句

let a=1; #计算表达式
test -z a; #类似[ -z a ]
exit 1; #退出shell脚本,并返回1,非0表示出现错误。
. file # 执行file内容,等价source file
export a; # 让被调用的shell命令继承定义的a变量
ls x*; # x* 表示让shell从当前工作目录匹配x开始的路径

通配glob匹配规则:

  • * 匹配0到n个字符
  • ? 表示匹配一个字符
  • [字符] 一次匹配括号内任意一个字符
  • [!字符] 一次匹配非括号内的一个字符
  • {字符串2,字符串2} 一次匹配大括号内中对应的一个字符串
  • {1..100} 同上,表示1到100的序列
cat < a; #重定向读取a文件,并通过cat输出到标准输出

重定向:

  • /dev/stdin 标准输入,文件描述符0
  • /dev/stdout 标准输出,文件描述符1
  • /dev/stderr 标准错误输出,文件描述符2
  • > 输出重定向
  • <输入重定向
  • 1>&0 等价重定向
  • >> 追加输出重定向
  • << 追加输入重定向
  • | 管道,a|b 等于 a > /dev/stdin; b < /dev/stdin;

变量中的变量:

let a=1
x='long$a' #x是单引号字符串long$a,而不是long1
#怎样通过x获得a的值?
# 通过二次解析shell参数命令:eval
$(eval echo $x) 
#1. shell将eval echo $x解析为 eval echo long$a
#2. eval 将参数再次提交给shell解析,eval echo long1
#3. eval执行命令,得long1

# 注意,如果x只是一个使用a变量的表达式,可以直接计算,而不需要经过shell进行符号解析
x='a+1'
$[$x] #直接计算a+1 => 1+1 => 2

变量空值短语:

let x="";
a=${x:-string} # 如果x是空,用string取代它
a=${x:=string} # 同上,并设置x=string
a=${x:+string} # 如果x不空,string取代它
a=${x:?string} # 如果x为空,报错string并退出

变量模式匹配短语:

x="asIas"
a=${x%a*s} # 从右去除最短匹配,得asI
a=${x%s*s} # 从右去除最长匹配,得a
a=${x#a*s} # 从左去除最短匹配,得Ias
a=${x##a*a} # 从左去除最长匹配,得s

默认变量

脚本环境自带许多有用的默认变量。

符号
~ 用户目录路径
~user 指定用户目录
~+ 当前目录,类似$(PWD)
$# 参数总数
$? 命令的返回值
$0 命令本身
$1 第一个参数
$2 第二个参数,依此类推
$* 所有参数的字符串
$@ 参数数组

流程语句

  1. if 条件; then 命令; fi
  2. if 条件; then 命令1; else 命令2; fi
  3. if 条件1; then 命令1; elif 条件2; then 命令2; ... fi;
  4. case word in 模式1) 命令1;; 模式2)命令2;; ... esac
  5. while 条件;do 命令;done
  6. for 变量 in 数组; do 命令; done
  7. 函数(){ 命令; } #注意大括号内侧空格
  8. 重定向不但可以针对某个命令,还能针对语句段,这时它是一个上下文概念,表示替换掉标准的输入或输出
    1. while read line;do echo $line;done < file : 循环读取文件
  9. 进程替换(process substitution)
    1. cat <(ls) : <(ls) 替换为命名管道(可读),作为 cat 的输入
      1. cat /dev/fd/63 <- ls: 类似
      2. ls -lh <(ls): 显示 /dev/fd/63 -> 'pipe:[410657]'
    2. ls > >(cat):>(cat) 替换为命名管道(可写),作为 ls 的输出
      1. ls > /dev/fd/63 -> cat: 类似
      2. 注意,>(...) 是替换成命名管道文件,而第一个 > 是将 ls 的输出重定向到管道文件,这是两个不同的点
      3. ls -lh >(cat) : 显示 /dev/fd/63 -> 'pipe:[403332]'

编辑内容文件test.sh:

#!/bin/bash
#脚本文件第一行的标准写法如上,表示调用/bin/bash脚本解释器

# 注意防止$1$2为空,需要将它放进引号内
if [ -f "$1" ] && [ -f "$2" ];then
	echo newfile:$1$2;
	cat $1 $2 > $1$2; #合并参数1,参数2的文件内容
fi

# 函数有自己独立的参数 $1 等变量
help(){
	cat <<EOF
使用说明:
	-a 输出-a
	-b xxx 输出 xxx
	-h 显示本页信息
EOF

local a=1 # 定义局部变量
}

let index=1; #定义保存序号的变量,从1开始编号
for var in "$@";do #读取命令参数数组
   echo arg$index:$var; #打印位置和参数
   index+=1; #位置+1
done

while [ -n "$1" ];do #如果参数1存在,循环
   case "$1"
    in
		-h) help;shift 1;; #shift 1 跳过一个参数
		-a) echo $1;shift 1;; #匹配 -a 选项
		-b) echo $2;shift 2;; #匹配 -b 选项
		--) shift;break;; #参数结束,跳出循环
		-*) echo 没有$1选项。;exit 1;; #没有定义的选项,退出shell程序
		*) break;; # 其他情况,跳出循环
   esac
done
 
while :
do
   echo 无限循环
done

执行脚本:

chmod +x test.sh #添加执行权限给脚本文件
./test.sh #linux中调用不在$PATH路径变量下的可执行程序,都要给出具体路径。如./xxx 而不能省略为 xxx

shebang 段

#!/usr/bin/env bash
# 脚本第一行#!开头,下面会打开指定程序(即脚本解释器)
# /usr/bin/env 是一个路径固定的程序,它能够依据环境变量
# 再找到 bash 然后打开
# 有一些是这样 #!/bin/sh  。sh 一般会链接到 bash
# 没有指定解释器,那么也可以,系统一般都会找到
# 但用户可能配置了不同的 shell,那么就有问题

字符串处理

模式匹配:

  1. *:任意字符串
  2. ?:一个字符
  3. [...]:字符集,如[ab] 表示 a 或 b
    1. [^...]:取反
  4. 扩展 extglob
    1. ?(模式):0-1次
    2. *(模式):0-n次
    3. +(模式):1-n次
    4. @(模式):1次
    5. !(模式):取反
a="aabbaacc"
${#a} # 字符串长度
${a:1:3} # 子串 tri

m=? # 模式,可以使用通配符
${a#m} # 从头开始,最短匹配,并删除
${a##m} # 从头开始,最长匹配,并删除
${a%m} # 从后开始,最短匹配,并删除
${a%%m} # 从后开始,最长匹配,并删除
${a#+(a)} # abbaacc
${a##+(a)} # bbaacc
${a%+(c)} # aabbaac
${a%%+(c)} # aabbaa

${a/m/"x"} # 单次匹配,并替换
${a//m/"x"} # 多次匹配,并替换
${a/a/x} # xabbaacc
${a//a/x} # xxbbxxcc

bash 怪癖总结

let a=1 # 等号要连着
let a=2 # 想修改值时可以重复设置
a=1 # 有 let 将视为表达式,没 let 视为字符串 '1'
let b=a+a # 如果要让变量按数字理解,得 2
b=$[a+a] # 或这样。用 $[a+a] 计算,得 2 
b=a+a # 得 'a+a'
b=$a+$a # 得 ‘1+1’
let b=$a$a # 如果只是取值,他会按字符串拼接到一起, 得 '11'
b="$a" # 变量在字符串中也是可以使用,当 $a 为空时,得到空字符串。比如在判断中他们是不同的
unset b # 设置空值

export c=1 # 设置环境变量,它不但在当前脚本有效,子命令也有效
local d=1 # 在函数内定义局部变量

# 条件
# 条件 [ 判断其实也是一个命令,所以不要紧贴着参数,即 [ a 而不是 [a
# 虽然人眼看上去能区分,但 bash 语法没那么精细
# [ x ] 单个条件,注意中括号不要连着条件
# ! [ x ] 否定条件,注意 ! 号不要连着条件
# [[ ! x ]] 或者这样
# [[ x && y ]] 多个条件
if [ 1 -eq 1 ]; then echo $?;else echo $?;fi
# 注意 if 语句本质是几个语句连在一起,所以需要 ; 分割,除非不在一行

调试

set -x # 显示当前执行的语句
set +x # 关闭

set -e # 遇到错误(语句返回非0)立即退出
set +e # 只提示,接着执行后续语句

command || exit 1; # 遇到错误立即退出

set -eo pipefail # 包括管道任意位置的错误也将立即退出

pipe=$(mktemp -u) # 创建临时文件
trap "rm $pipe" EXIT # 任何情况退出都删掉命名管道
mkfifo $pipe # 创建命名管道,进程间通信
echo sss | cat >$pipe & # 后台任务,写入管道
read msg < $pipe && echo $msg # 读取管道内容,这套操作可以跨进程

高级脚本

使用管道组合多个命令

a | b | c : a 的输出进入 b 的输入,b 的输出进入 c 的输入。系统实际上使用子进程来执行,所以 a b c 是当前程序的子程序。

export E 环境变量可以从当前程序传到 abc 但,他们无法修改,修改后的结果无法传递回父进程。这是一个单向通信的方式。

为了双向通信,需要使用到一个叫命名管道的技术,这是 linux 进程通信的技术。

mkfifo 命令创建一个管道文件。这有点类似匿名管道, a | b | c,一个地方写入,另一个地方读取。但 a | func 是不行的,因为 func 本身又变成子程序了。我们要让子程序和当前程序通信,需要 a > pipe。 但写入的时候,就会等待另外的地方读取,程序就卡住,阻塞了。

所以,需要使用后台执行的方式, a > pipe & 。只需要加 & 在一行命令后面,它就在后台执行,不会阻塞当前程序。

要如何读取管道数据,到变量并没有想像中那么简单,因为变量不接受管道输入 var < pipe 是不合法的。这时需要使用一个 read 命令,它可以读取管道数据,并填充到变量中。

但 read 一次只能读一行。所以需要我们使用循环语句来读取所有数据。

问题来了,如何用循环正确读取管道数据?

# 命名管道
trap 'rm -v pipe' EXIT
mkfifo pipe
man while | cat > pipe &
while read line
do
   echo $line
done < pipe

# 使用进程替换技术
while read line
do
   echo $line
done < <(man while)

匿名管道的输出其实有两个,一个是正常消息,一个是错误消息。有一些命令的错误消息并不是那么容易捕获。

# time 计时信息通过错误通道传递
# tr 转换成大写
# 但代码不生效
time sleep 1 2>&1 | tr 'a-z' 'A-Z'

# 需要大括号包裹起来作为一个语句段来重定向
# time 实际将错误传递给 sleep 了,所以无法正常方式捕获
{ time sleep 1; } 2>&1 | tr 'a-z' 'A-Z'

管道之间是通过读取管道来执行,但是有些命令,它并不读取管道,它接受普通的参数。这时候就需要 xargs 命令,它读取管道,然后将其转化为参数,提供给下一个命令。

# 无效,因为 echo 不接受管道输入
echo 1+1 |echo

# xargs 将读取的所有输入替换成参数序列
echo 1+1 | xargs echo # 相当于 echo 1+1

# 计算器例子
# 第一段负责输入式子
# 第二段转换成既定格式
# 第三段负责计算
# -I_ 指定参数替换到 _ 上
echo "4*5.7^3*sqrt(3+69%10)/3.14" | xargs -I_ echo "scale=2;_" | bc

常见命令组合

  1. 搜索文件: find
  2. 查找内容: grep
  3. 取某列: cut
  4. 排序: sort
  5. 去重: quiq
  6. 替换: tr
  7. 统计: wc
find /usr/bin -name "ls*" -exec realpath {} \; | xargs -n1 du | cut -f 1 | sort -nr | xargs | { read line; echo $line; }

行列转换

命令的输出可以多行,如果对一行的一部分处理(即所谓的列)。

命令是一股脑输出所有的结果的。因此接受该输入的命令要能处理多行。并不是所有命令都能支持多行的。比如 ls 只能接受单行的参数。

那么就必须使用:"多行 --> 多次调用单行命令" 的模式。

还是老朋友 xargs。

有一些命令,它可以针对每一行额外调用一个命令,如 find -exec ls -hdl {} \;

但是最有效的还是借助两个强大的命令:

  1. sed:单行处理, 参考:sed 行处理
  2. awk:多行处理(列)
# 多行 -> 多次单行
ls -l | xargs -i echo {}

# 单行 -> 多次分列
# sed 一次读取一行,然后分别对该行执行命令
# -n 只显示匹配行
# -r 正则
# [[:blank:]] 空白字符集
# [^[:blank:]] 非空白字符集
# () 分组,\1 引用第一个分组匹配的结果
# 1 命令只对第一行有效
# s/a/b/g ,s 字符串替换命令
# 其中:a 匹配部分,b 替换部分, g 全局标记,表示多次替换(空为一次)
# p 打印
# ; 命令分组分隔符,不同组独立重新执行。没分隔符时是继续执行
# 1{a;b} a;b 都只对第一行有效
# 整体就是以空格为分隔符,将多列转化为多行
ls -l | sed -nr '1s/([^[:blank:]]+)([[:blank]]+)/\1\n/gp' | xargs -i echo {}

# 多行 -> 多次单行 -> 多次分列
ls -l | awk  '{if(NR==1) ; else {if(NR==3) for(i=1;i<=NF;i++) print $i; else for(i=1;i<=NF-1;i++)print $i}}' | xargs -i echo {}

ash

这是 alpine linux 中默认使用的 shell,这个 shell 符合 posix 标准,移植性比较好,但功能稍微弱一些。

  1. list(列表):语句序列
    1. (list) : 在子进程执行
    2. { list; }
  2. a && b || c : a 成功(0)执行 b,b 失败(!0)执行 c
  3. a | b : a 的输出作为 b 的输入,返回值为 b
  4. if list;then list; elif list; then list;else list; fi : 条件语句
  5. while list;do list; done: 循环语句
    1. break [n]:跳出 n 个循环
    2. continue [n]:跳到 n 个循环的下一步
  6. for iter in words; do list; done:迭代语句
  7. case words in pattern) list;; esac:分支语句
  8. pattern(模式):
    1. *: 任意序列
    2. ?:单个字符
    3. [abc]: 字符集,即 a 或 b 或 c
    4. !:取反
  9. name() command: 函数
    1. local name: 局部变量
    2. $1:独立的位置参数
    3. return [status]: 返回值
  10. 重定向,修改默认的输出或输入,过后回复成默认值
    1. <、>、>>、<&0、>&1
    2. << EOF :由用户输入内容,直到输入 EOF 结束
  11. 命令替换:输出结果替换到当前位置
    1. `list`
    2. $(list)
  12. 分词:单词分割
    1. $IFS:分隔符
    2. $@:分割:"$1" "$2" ... "$n"
    3. $*: 组合:"$1 $2 ... $n"

bash -> ash

// bash
[[ "$a" == "$b" ]]
// ash
[ "$a" == "$b" ]

参考

  1. shell解析命令行的过程以及eval命令: https://www.cnblogs.com/f-ck-need-u/p/7426371.html
  2. shell中的括号(小括号,中括号,大括号): https://blog.csdn.net/tttyd/article/details/11742241
  3. Bash 脚本教程:https://wangdoc.com/bash/condition
  4. xargs 命令:https://www.runoob.com/linux/linux-comm-xargs.html
  5. man bash
  6. 进程替换:https://en.wikipedia.org/wiki/Process_substitution#:~:text=In%20computing%2C%20process%20substitution%20is,occur%2C%20by%20the%20command%20shell.
  7. shellcheck 检测脚本文件是否符合标准写法:https://www.shellcheck.net/
1
https://gitee.com/coder_lw/wiki.git
git@gitee.com:coder_lw/wiki.git
coder_lw
wiki
wiki
master

搜索帮助