30min速成Shell脚本

一、shell是什么?

我们常说的shell脚本,是一种为shell编写的脚本程序,shell编程非常简洁,只需要在有一个能写代码的文本编辑器,比如上文提到的vim,和能解释执行的脚本解释器就行。先看一下你的电脑里装了哪些shell呢?

输入: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

一般我们常用的是Bash,也就是Bourne Again Shell,也是大多数Linux系统默认的shell,不过我们基本不会可以区分Bourne Shell 和 Bourne Again Shell,所以像 #!/bin/sh,它同样也可以改为 #!/bin/bash

#! 意思是告诉系统其后路径所指定的程序即是解释此脚本文件的 Shell 程序

如何运行shell文件?

  • **1.**作为可执行程序

vi hello.sh

chmod +x ./test.sh #使脚本具有执行权限

./test.sh #执行脚本

注意执行的时候一定要写成 ./test.sh,而不是 test.sh,运行其它二进制的程序也一样,直接写 test.sh,linux 系统会去 PATH 里寻找有没有叫 test.sh 的,而只有 /bin, /sbin, /usr/bin,/usr/sbin 等在 PATH 里,你的当前目录通常不在 PATH 里,所以写成 test.sh 是会找不到命令的,要用 ./test.sh 告诉系统说,就在当前目录找。

  • 2.作为解释器参数

这种运行方式是,直接运行解释器,其参数就是 shell 脚本的文件名,如:

sh test.sh
bash test.sh

二、变量和参数

2.1 变量

#!/bin/bash
# 脚本的第一行叫 shebang,用来告知系统如何执行该脚本:

# 显示 “Hello world!”
echo Hello world!

# 每一句指令以换行或分号隔开:
echo 'This is the Day One Tutorial'; echo 'This is Day Two Tutorial'

# 声明一个变量:
a="Hello TinyMS"

# 下面是错误的做法:
a = "Hello TinyMS"
# Bash 会把 a 当做一个指令,由于找不到该指令,因此这里会报错。

# 也不可以这样:
a= 'Hello TinyMS'
# Bash 会认为 'Hello,TinyMS' 是一条指令,由于找不到该指令,这里再次报错。
# (这个例子中 'a=' 这部分会被当作仅对 'Hello,TinyMS' 起作用的赋值。)

# 使用变量:
echo $a
echo "$a"
echo '$a'
# 当你赋值 (assign) 、导出 (export),或者以其他方式使用变量时,变量名前不加 $。
# 如果要使用变量的值, 则要加 $。
# 注意: ' (单引号) 不会展开变量(即会屏蔽掉变量)。

# 在变量内部进行字符串代换
echo ${a/Hello/Hi}
# 会把 a 中首次出现的 "some" 替换成 “A”。

# 变量的截取
Length=6
echo ${a:0:Length}
# 这样会仅返回变量值的前7个字符

# 变量的默认值
echo ${a:-"DefaultValueIfFooIsMissingOrEmpty"}
# 对 null (Foo=) 和空串 (Foo="") 起作用; 零(Foo=0)时返回0
# 注意这仅返回默认值而不是改变变量的值

几个内置变量

参数处理 说明
$# 传递到脚本的参数个数
$* 以一个单字符串显示所有向脚本传递的参数。 如"$*"用「"」括起来的情况、以"$1 $2 … $n"的形式输出所有参数。
$$ 脚本运行的当前进程ID号
$! 后台运行的最后一个进程的ID号
$@ 与$*相同,但是使用时加引号,并在引号中返回每个参数。 如"$@"用「"」括起来的情况、以"$1" "$2" … "$n" 的形式输出所有参数。
$- 显示Shell使用的当前选项,与set命令功能相同。
$? 显示最后命令的退出状态。0表示没有错误,其他任何值表明有错误。
# 内置变量:
echo "Last program return value: $?"
echo "Script's PID: $$"
echo "Number of arguments: $#"
echo "Scripts arguments: $@"
echo "Scripts arguments separated in different as: $1 $2..."

$* 与 $@ 区别:

  • 相同点:都是引用所有参数。

  • 不同点:只有在双引号中体现出来。假设在脚本运行时写了三个参数 1、2、3,,则 “ * “ 等价于 “1 2 3”(传递了一个参数),而 “@” 等价于 “1” “2” “3”(传递了三个参数)。

#!/bin/bash

echo "-- \$* example ---"
for i in "$*"; do
    echo $i
done

echo "-- \$@ example ---"
for i in "$@"; do
    echo $i
done

执行脚本,输出结果如下所示:

$ chmod +x test.sh 
$ ./test.sh 1 2 3
-- $* example ---
1 2 3
-- $@ example ---
1
2
3

2.2 传递参数

  • 读取输入:

echo "What's your name?"
read Name # 这里不需要声明新变量,输入自己的名字
# 在屏幕输入你的名字,这里我输入的是Charlotte
echo Hello, $Name!

输出结果:

Hello,Charlotte
  • 传递参数

#!/bin/bash

echo "Shell 传递参数!";
echo "执行的文件名:$0";
echo "第一个参数为:$1";
echo "第二个参数为:$2";
echo "第三个参数为:$3";

输出结果:

$ sh test.sh 1 2 3
Shell 传递参数实例!
执行的文件名:./test.sh
第一个参数为:1
第二个参数为:2
第三个参数为:3

三、shell数组

Bash Shell 只支持一维数组(不支持多维数组),初始化时不需要定义数组大小(与 PHP 类似)。

  • 创建数组

格式:

array_name=(value1 value2 ... valuen)

举例:

test_array=(this is shell tutorial)
  • 读取数组

格式:

${array_name[index]}

举例:

#!/bin/bash

test_array=(this is shell tutorial)

echo "第一个元素为: ${test_array[0]}"
echo "第二个元素为: ${test_array[1]}"
echo "第三个元素为: ${test_array[2]}"
echo "第四个元素为: ${test_array[3]}"

输出结果:

root@73df419e7539:~# sh array.sh
第一个元素为: this
第二个元素为: is
第三个元素为: shell
第四个元素为: tutorial
  • 获取数组中的所有元素

#!/bin/bash

test_array[0]=this
test_array[1]=is
test_array[2]=shell
test_array[3]=tutorial

echo "数组的元素为: ${test_array[*]}"
echo "数组的元素为: ${test_array[@]}"
  • 获取数组的长度

#!/bin/bash

test_array[0]=this
test_array[1]=is
test_array[2]=shell
test_array[3]=tutorial

echo "数组的元素为: ${#test_array[*]}"
echo "数组的元素为: ${#test_array[@]}"
  • 用for 循环遍历数组

#!/bin/bash
  arr=(1 2 3 4 5 6 7 8 9 10)
  for a in ${arr[*]}
  do
    echo $a
  done
  • 用while循环输出数组

arr=(1 2 3 4 5 6 7 8 9 10)
i=0
while [ $i -lt ${#arr[@]} ]
do
  echo ${arr[$i]}
  let i++
done

四、shell运算符

Shell 和其他编程语言一样,支持多种运算符,包括:

  • 算数运算符

  • 关系运算符

  • 布尔运算符

  • 字符串运算符

  • 文件测试运算符

例如两数相加:

#!/bin/bash

val=`expr 2 + 2` 
val_1=$(expr 10 + 20)
echo "两数之和为 : $val"
echo "两数之和为 : $val_1"

两点注意:

  • 表达式和运算符之间要有空格,例如 2+2 是不对的,必须写成 2 + 2。

  • 完整的表达式要被 `` 包含,注意这个字符不是常用的单引号,在 Esc 键下边。

算术运算符

关系运算符

关系运算符只支持数字,不支持字符串,除非字符串的值是数字。

下表列出了常用的关系运算符,假定变量 a 为 10,变量 b 为 20:

运算符 说明 举例
-eq 检测两个数是否相等,相等返回 true。 [ $a -eq $b ] 返回 false。
-ne 检测两个数是否不相等,不相等返回 true。 [ $a -ne $b ] 返回 true。
-gt 检测左边的数是否大于右边的,如果是,则返回 true。 [ $a -gt $b ] 返回 false。
-lt 检测左边的数是否小于右边的,如果是,则返回 true。 [ $a -lt $b ] 返回 true。
-ge 检测左边的数是否大于等于右边的,如果是,则返回 true。 [ $a -ge $b ] 返回 false。
-le 检测左边的数是否小于等于右边的,如果是,则返回 true。 [ $a -le $b ] 返回 true。

EQ 就是 EQUAL等于

NE 就是 NOT EQUAL不等于

GT 就是 GREATER THAN大于

LT 就是 LESS THAN小于

GE 就是 GREATER THAN OR EQUAL 大于等于

LE 就是 LESS THAN OR EQUAL 小于等于

示例:

#!/bin/bash

a=10
b=20

if [ $a -eq $b ]
then
   echo "$a -eq $b : a 等于 b"
else
   echo "$a -eq $b: a 不等于 b"
fi
if [ $a -ne $b ]
then
   echo "$a -ne $b: a 不等于 b"
else
   echo "$a -ne $b : a 等于 b"
fi
if [ $a -gt $b ]
then
   echo "$a -gt $b: a 大于 b"
else
   echo "$a -gt $b: a 不大于 b"
fi
if [ $a -lt $b ]
then
   echo "$a -lt $b: a 小于 b"
else
   echo "$a -lt $b: a 不小于 b"
fi
if [ $a -ge $b ]
then
   echo "$a -ge $b: a 大于或等于 b"
else
   echo "$a -ge $b: a 小于 b"
fi
if [ $a -le $b ]
then
   echo "$a -le $b: a 小于或等于 b"
else
   echo "$a -le $b: a 大于 b"
fi

输出结果:

10 -eq 20: a 不等于 b
10 -ne 20: a 不等于 b
10 -gt 20: a 不大于 b
10 -lt 20: a 小于 b
10 -ge 20: a 小于 b
10 -le 20: a 小于或等于 b

布尔运算符

下表列出了常用的布尔运算符,假定变量 a 为 10,变量 b 为 20:

运算符 说明 举例
! 非运算,表达式为 true 则返回 false,否则返回 true。 [ ! false ] 返回 true。
-o 或运算,有一个表达式为 true 则返回 true。 [ $a -lt 20 -o $b -gt 100 ] 返回 true。
-a 与运算,两个表达式都为 true 才返回 true。 [ $a -lt 20 -a $b -gt 100 ] 返回 false。

示例

#!/bin/bash

a=10
b=20

if [ $a != $b ]
then
   echo "$a != $b : a 不等于 b"
else
   echo "$a == $b: a 等于 b"
fi
if [ $a -lt 100 -a $b -gt 15 ]
then
   echo "$a 小于 100 且 $b 大于 15 : 返回 true"
else
   echo "$a 小于 100 且 $b 大于 15 : 返回 false"
fi
if [ $a -lt 100 -o $b -gt 100 ]
then
   echo "$a 小于 100 或 $b 大于 100 : 返回 true"
else
   echo "$a 小于 100 或 $b 大于 100 : 返回 false"
fi
if [ $a -lt 5 -o $b -gt 100 ]
then
   echo "$a 小于 5 或 $b 大于 100 : 返回 true"
else
   echo "$a 小于 5 或 $b 大于 100 : 返回 false"
fi

输出结果:

10 != 20 : a 不等于 b
10 小于 100  20 大于 15 : 返回 true
10 小于 100  20 大于 100 : 返回 true
10 小于 5  20 大于 100 : 返回 false

逻辑运算符

运算符 说明 举例
&& 逻辑的 AND [[ $a -lt 100 && $b -gt 100 ]] 返回 false
|| 逻辑的 OR [[ $a -lt 100 || $b -gt 100 ]] 返回 true

示例

#!/bin/bash

a=10
b=20

if [[ $a -lt 100 && $b -gt 100 ]]
then
   echo "返回 true"
else
   echo "返回 false"
fi

if [[ $a -lt 100 || $b -gt 100 ]]
then
   echo "返回 true"
else
   echo "返回 false"
fi

输出结果:

返回 false
返回 true

字符串运算符

下表列出了常用的字符串运算符,假定变量 a 为 “abc”,变量 b 为 “efg”:

运算符 说明 举例
= 检测两个字符串是否相等,相等返回 true。 [ $a = $b ] 返回 false。
!= 检测两个字符串是否不相等,不相等返回 true。 [ $a != $b ] 返回 true。
-z 检测字符串长度是否为0,为0返回 true。 [ -z $a ] 返回 false。
-n 检测字符串长度是否不为 0,不为 0 返回 true。 [ -n "$a" ] 返回 true。
$ 检测字符串是否为空,不为空返回 true。 [ $a ] 返回 true。

示例:

#!/bin/bash

a="abc"
b="efg"

if [ $a = $b ]
then
   echo "$a = $b : a 等于 b"
else
   echo "$a = $b: a 不等于 b"
fi
if [ $a != $b ]
then
   echo "$a != $b : a 不等于 b"
else
   echo "$a != $b: a 等于 b"
fi
if [ -z $a ]
then
   echo "-z $a : 字符串长度为 0"
else
   echo "-z $a : 字符串长度不为 0"
fi
if [ -n "$a" ]
then
   echo "-n $a : 字符串长度不为 0"
else
   echo "-n $a : 字符串长度为 0"
fi
if [ $a ]
then
   echo "$a : 字符串不为空"
else
   echo "$a : 字符串为空"
fi

输出结果:

abc = efg: a 不等于 b
abc != efg : a 不等于 b
-z abc : 字符串长度不为 0
-n abc : 字符串长度不为 0
abc : 字符串不为空

下表列出了常用的算术运算符,假定变量 a 为 10,变量 b 为 20:

运算符 说明 举例
+ 加法 expr $a + $b 结果为 30。
- 减法 expr $a - $b 结果为 -10。
* 乘法 expr $a \* $b 结果为 200。
/ 除法 expr $b / $a 结果为 2。
% 取余 expr $b % $a 结果为 0。
= 赋值 a=$b 将把变量 b 的值赋给 a。
== 相等。用于比较两个数字,相同则返回 true。 [ $a == $b ] 返回 false。
!= 不相等。用于比较两个数字,不相同则返回 true。 [ $a != $b ] 返回 true。

**注意:**条件表达式要放在方括号之间,并且要有空格,例如: [$a==$b] 是错误的,必须写成 [ $a == $b ]

#!/bin/bash

a=10
b=20

val=`expr $a + $b`
echo "a + b : $val"

val=`expr $a - $b`
echo "a - b : $val"

val=`expr $a \* $b`
echo "a * b : $val"

val=`expr $b / $a`
echo "b / a : $val"

val=`expr $b % $a`
echo "b % a : $val"

if [ $a == $b ]
then
   echo "a 等于 b"
fi
if [ $a != $b ]
then
   echo "a 不等于 b"
fi

输出结果:

a + b : 30
a - b : -10
a * b : 200
b / a : 2
b % a : 0
a 不等于 b

五、Shell test命令

Shell中的 test 命令用于检查某个条件是否成立,它可以进行数值、字符和文件三个方面的测试。

数值测试

参数 说明
-eq 等于则为真
-ne 不等于则为真
-gt 大于则为真
-ge 大于等于则为真
-lt 小于则为真
-le 小于等于则为真

示例

num1=100
num2=100
if test $[num1] -eq $[num2]
then
    echo '两个数相等!'
else
    echo '两个数不相等!'
fi

输出结果:

两个数相等!

判断文件夹/文件是否存在

文件夹不存在则创建

if [ ! -d "/root/test" ];then
  mkdir /root/test
  else
  echo "文件夹已经存在"
fi

文件存在则删除

if [ ! -f "/root/test1.sh" ];then
  echo "文件不存在"
  else
  rm -f /root/test1.sh
fi

判断文件夹是否存在

if [ -d "/data/" ];then
  echo "文件夹存在"
  else
  echo "文件夹不存在"
fi

判断文件是否存在

if [ -f "/data/filename" ];then
  echo "文件存在"
  else
  echo "文件不存在"
fi

六、使用函数

定义函数

function foo ()
{
    echo "Arguments work just like script arguments: $@"
    echo "And: $1 $2..."
    echo "This is a function"
    return 0
}

更简单的方法:

# 更简单的方法
bar ()
{
    echo "Another way to declare functions!"
    return 0
}

调用函数

foo "My name is" $Name

输出结果

root@2097f8ce4358:~# foo "My name is" $Name
Arguments work just like script arguments: My name is Charlotte
And: My name is Charlotte...
This is a function

七、输入输出重定向

常用场景:前一个指令的输出可以当作后一个指令的输入,在深度学习中启动训练时通常可以把运行训练的结果输出到log文件中,或者作为下游任务的输入,以下列举了一些常用的命令

# 用下面的指令列出当前目录下所有的 txt 文件:
ls -l | grep "\.txt"

# 重定向输入和输出(标准输入,标准输出,标准错误)。
# 以 ^EOF$ 作为结束标记从标准输入读取数据并覆盖 hello.py :
cat > hello.py << EOF
#!/usr/bin/env python
from __future__ import print_function
import sys
print("#stdout", file=sys.stdout)
print("#stderr", file=sys.stderr)
for line in sys.stdin:
    print(line, file=sys.stdout)
EOF

# 重定向可以到输出,输入和错误输出。
python hello.py < "input.in"
python hello.py > "output.out"
python hello.py 2> "error.err"
python hello.py > "output-and-error.log" 2>&1
python hello.py > /dev/null 2>&1
# > 会覆盖已存在的文件, >> 会以累加的方式输出文件中。
python hello.py >> "output.out" 2>> "error.err"

# 覆盖 output.out , 追加 error.err 并统计行数
info bash 'Basic Shell Features' 'Redirections' > output.out 2>> error.err
wc -l output.out error.err

# 运行指令并打印文件描述符 (比如 /dev/fd/123)
# 具体可查看: man fd
echo <(echo "#helloworld")

# 以 "#helloworld" 覆盖output.out,以下三种方式均可:
cat > output.out <(echo "#helloworld")
echo "#helloworld" > output.out
echo "#helloworld" | cat > output.out
echo "#helloworld" | tee output.out >/dev/null

# 清理临时文件并显示详情(增加 '-i' 选项启用交互模式)
rm -v output.out error.err output-and-error.log

# 一个指令可用 $( ) 嵌套在另一个指令内部:
# 以下的指令会打印当前目录下的目录和文件总数
echo "There are $(ls | wc -l) items here."

# 反引号 `` 起相同作用,但不允许嵌套
# 优先使用 $(  ).
echo "There are `ls | wc -l` items here."

八、流程控制函数

# Bash 的 case 语句与 Java 和 C++ 中的 switch 语句类似:
case "$a" in
    # 列出需要匹配的字符串
    0) echo "There is a zero.";;
    1) echo "There is a one.";;
    *) echo "It is not null.";;
esac

# 循环遍历给定的参数序列:
# 变量$a 的值会被打印 3 次。
for a in {1..3}
do
    echo "$a"
done

# 或传统的 “for循环” :
for ((a=1; a <= 3; a++))
do
    echo $a
done

# 也可以用于文件
# 用 cat 输出 file1 和 file2 内容
for a in test2.sh test3.sh
do
    cat "$a"
done

# 或作用于其他命令的输出
# 对 ls 输出的文件执行 cat 指令。
for Output in $(ls)
do
    cat "$Output"
done

# while 循环:
while [ true ]
do
    echo "loop body here..."
    break
done

九、常见的文件操作命令

下面列举了一些常见的文件操作,帮助大家在实际开发中提升效率

# 打印 file.txt 的最后 10 行
tail -n 10 hello.txt
# 打印 file.txt 的前 10 行
head -n 10 hello.txt
# 将 file.txt 按行排序
sort hello.txt
# 报告或忽略重复的行,用选项 -d 打印重复的行
uniq -d hello.txt
# 打印每行中 ',' 之前内容
cut -d ',' -f 1 hello.txt
# 将 file.txt 文件所有 'hello' 替换为 'Hi', (兼容正则表达式)
sed -i 's/Hello/Hi/g' hello.txt
# 将 file.txt 中匹配正则的行打印到标准输出
# 这里打印以 "1" 开头, "MS" 结尾的行
grep "^1.*MS$" hello.txt
# 使用选项 "-c" 统计行数
grep -c "^1.*MS$" hello.txt
# 如果只是要按字面形式搜索字符串而不是按正则表达式,使用 fgrep (或 grep -F)
fgrep "^1.*MS$" hello.txt 

参考资料:

1.https://www.runoob.com/linux/linux-shell-io-redirections.html

2.https://jmt33.github.io/mtao/Html/Linux/20141022113029_shell.html

3.https://learnxinyminutes.com/docs/zh-cn/bash-cn/