Acwing - Linux基础课(三)- Shell

Shell语法

概述

Shell是我们通过命令行与操作系统沟通的语言

Shell是逐行解释执行的,它不需要编译,它可以直接运行。

Shell脚本可以直接在命令行运行,也可以将一套逻辑组织成一个文件,方便复用。

Linux里常见的Shell脚本,有很多,比如

  • Bourne Shell/usr/bin/sh/bin/sh
  • Bourne Again Shell/bin/bash
  • C Shell/usr/bin/csh
  • K Shell/usr/bin/ksh
  • zsh

Linux系统中一般默认用bash,所以接下来讲解bash中的语法

文件开头需要写上 #! /bin/bash,指明bash为脚本解释器。

示例

创建一个Shell脚本

vim test.sh

按下i,进入编辑模式,并输入如下内容

#! /bin/bash

echo "Hello,World"

运行方式

  1. 作为可执行文件

    给文件添加可执行权限

    chmod +x test.sh

    执行

    ./test.sh

  2. 用解释器执行

    bash test.sh

注释

单行注释:每行 # 之后的内容均是注释

# 这是注释
echo "Hello,World" # 注释

多行注释

:<<EOF
这是注释
这是注释
这是注释
EOF

其中EOF可以换成其他任意字符串,例如

:<<abc
注释
注释注释
abc

变量

定义变量

name1="hby"
name2='hby'
name3=hby
# 这三种写法等价,都是定义字符串

使用变量

需要加上$,或者${}{}是可选的,{}是用来帮助解释器识别变量名的边界的

echo $name1  # 输出hby 
echo ${name2}  # 输出hby
echo ${name2}haha  # 输出hbyhaha

只读变量

使用readonly或者declare进行声明,类似C++的const或者java的final

name=hby
readonly name  # 标记为只读
declare -r name  # 两种写法都可

name=abc # 会报错

可以用type命令来判断另一个命令是否是内置命令等,比如type readonlytype lstype cdtype pwdtype top

删除变量

使用unset即可

#! /bin/bash

name=hby
unset name

echo ${name} # 输出空字符串, 当一个变量不存在时, 其为空字符串

变量类型

  1. 自定义变量(局部变量)

    子进程不能访问的变量

  2. 环境变量(全局变量)

    子进程可以访问的变量

将自定义变量改为环境变量

root@yogurt:~$ name=hby # 自定义变量
root@yogurt:~$ export name # 改为环境变量
root@yogurt:~$ declare -x name # 改为环境变量

将环境变量改为自定义变量

root@yogurt:~$ export name=hby # 定义环境变量
root@yogurt:~$ declare +x name # 改为自定义变量

如何在一个bash里开一个子进程呢?

在一个命令行下,输入bash,会开启一个子进程,然后原先的进程会休眠,再输入exit,则会 从子进程退出到父进程。

root@yogurt:~$ echo $$ # 查看当前shell的pid
root@yogurt:~$ bash # 新开一个子进程
root@yogurt:~$ exit # 退出子进程

字符串

字符串可以用单引号,也可以用双引号,也可以不用引号。

单引号和双引号的区别:

  • 单引号中的内容会原样输出,不会执行,不会读取变量
  • 双引号中的内容,可以执行,可以读取变量
name=hby
echo 'hello, $name \"hh\"'
echo "hello, $name \"hh\""
echo hello, $name \"hh\"

不加引号的效果和双引号的一致

获取字符串的长度

name="hby"
echo ${#name} # 输出3

提取子串

name="hello,hby"
echo ${name:0:5} # 提取从0开始的5个字符
echo ${name:5} # 提取从5到字符串结尾的子串

默认变量

文件参数变量

在执行Shell脚本时,可以向脚本传递参数,$1是第一个参数,$2是第二个参数,以此类推,特殊的,$0是文件名(命令的第一段)

例,创建一个test.sh

#! /bin/bash
echo "文件名: "$0
echo "第1个参数: "$1
echo "第2个参数: "$2
echo "第3个参数: "$3
echo "第4个参数: "$4

比如执行

./test.sh

$0就是./test.sh

如果用绝对路径执行,比如

/home/yogurt/test.sh

$0就是 /home/yogurt/test.sh

其他相关变量

  • $# 文件传入的参数个数
  • $* 由所有参数构成的,由空格隔开的字符串, "$1 $2 $3 $4"
  • $@ 每个参数分别用双引号括起来的字符串, "$1" "$2" "$3" "$4"
  • $$ 脚本当前运行的进程ID
  • $? 上一条命令的退出状态,exit code,0表示正常退出,其他值表示错误
  • $(command) 返回command这条命令的输出结果(stdout)(注意区分输出结果和返回值)
  • `command` 返回 command 这条命令的stdout

数组

数组中可以存放多个不同类型的值,只支持一维数组,初始化时不需要指明数组大小,数组下标从0开始

定义

数组用小括号表示,元素之间用空格隔开,如

array=(1 abc "def" yogurt)

注意,数组元素都是字符串,无论是用双引号,单引号,或者不加引号

也可以直接用下标定义数组中某个元素的值

array[0]=1
array[1]=abc
array[2]="def"
array[3]=yogurt

数组下标定义比较灵活,中间位置的下标都可以不用定义,比如

array[0]=1
array[1]=abc
array[2]="def"
array[1000]=yogurt

读取

读取数组中某个元素的值

${array[index]}

array=(1 abc "def" yogurt)
echo ${array[0]}
echo ${array[1]}

读取整个数组

${array[@]}  #第一种写法
${array[*]}  #第二种写法

例如

#! /bin/bash
array[0]=1
array[1]=abc
array[1000]=yogurt
echo ${array[@]}  # 输出的时候会直接跳国中间没有值的部分
echo ${#array[@]} # 数组长度(数组中实际有多少元素), 输出3

expr命令

不是一个shell内建的命令,而是一个第三方命令

expr命令可以用来求表达式的值,格式为

expr 表达式

  • 由于expr是一个命令,所以后面表达式的每一项(参数),都要用空格隔开

  • 某一些特殊字符,需要用反斜杠来进行转义

  • 特殊字符,如空格,要用引号括起来

  • expr会在stdout中输出结果

字符串表达式

  • length STRING

    返回 STRING 的长度

    比如

    #! /bin/bash
    str="Hello World"
    echo $(expr length "$str") # 输出11
    
  • index STRING CHARSET

    CHARSET中,任意单个字符在STRING中最前面的字符位置,下标从1开始,如果在STRING中完全不存在CHARSET中的字符,则返回0

    比如

    #! /bin/bash
    str="Hello World"
    echo $(expr index "$str" aWd)
    # 在Hello World字符串中, 查找aWd中哪个字符最先出现
    # 最先出现的是W, 下标为7(下标从1开始)
    
  • substr STRING POSITION LENGTH

    返回STRING字符串中从POSITION开始(下标也从1开始),长度最大为LENGTH的子串。如果POSITIONLENGTH为负数,0,或非数值,则返回空字符串

    #! /bin/bash
    str="Hello World"
    echo $(expr substr "$str" 2 4) # 输出ello
    

整数表达式

expr支持算术操作,算术表达式优先级低于字符串表达式,高于逻辑关系表达式

  • + - 加减,两端参数会转换为整数,转换失败时会报错
  • * / % 乘除,取模
  • () 可以改变优先级,但需要用反斜杠转义

例如

a=3
b=4

echo `expr $a + $b`  # 输出7
echo `expr $a - $b`  # 输出-1
echo `expr $a \* $b`  # 输出12,*需要转义
echo `expr $a / $b`  # 输出0,整除
echo `expr $a % $b` # 输出3
echo `expr \( $a + 1 \) \* \( $b + 1 \)`  # 输出20,值为(a + 1) * (b + 1)

逻辑关系表达式

  • |

    或,遵循短路原则。如果第一个参数非空或非零,则返回第一个参数的值,否则返回第二个参数的值,但要求第二个参数的值也是非空或非零,否则返回0。如果第一个参数是非空或非零,则不会计算第二个参数

  • &

    与,遵循短路原则。如果2个参数都非空非0,则返回第一个参数,否则返回0。

  • < <= = == != >= >

    比较大小。===是同义词。比较两端的参数,如果为true,则返回1,否则返回0。expr会首先尝试将两端参数转换为整数,并作算术比较,如果转换失败,则按字符集排序规则做字符串比较。

a=3
b=4

echo `expr $a \> $b`  # 输出0,>需要转义
echo `expr $a '<' $b`  # 输出1,也可以将特殊字符用引号引起来
echo `expr $a '>=' $b`  # 输出0
echo `expr $a \<\= $b`  # 输出1

c=0
d=5

echo `expr $c \& $d`  # 输出0
echo `expr $a \& $b`  # 输出3
echo `expr $c \| $d`  # 输出5
echo `expr $a \| $b`  # 输出3

read命令

read命令用于从标准输入中读取单行数据

参数说明

  • -p :后面可以添加提示信息
  • -t:后面跟秒数,定义输入字符的等待时间,超过等待时间后会忽略该命令

例如

#! /bin/bash

read -p "What's your name? " name
echo Hello, $name

read命令正常读入字符串时,其exit code为0,表示正常退出。

当读入了文件结束符EOF(按下Ctrl + d) 时,read命令的exit code为1。

echo命令

用于输出字符串,会自动在末尾加上换行符

echo STRING

显示普通字符串

echo "Hello World"
echo Hello World  # 引号可以省略

显示转义字符串

echo "\"Hello,World\"" # 双引号或者不加引号
# 单引号会原样输出, 不会转义

显示变量

name=yogurt
echo "my name is $name"

显示换行

echo -e "Hello\nWorld" # -e开启转义, esacpe

显示不换行

echo -e "Hello \c" # \c取消换行
echo "World"
# 由于 echo 会自动在输出完后换行, 如果想要2个echo输出在同一行, 可以用 \c 取消换行

显示的结果定向至文件

echo "Hello,World" > output.txt

原样输出字符串,用单引号即可

显示命令执行结果

echo `date`

printf命令

该命令用于格式化输出,类似于C/C++中的printf函数

默认不会在结尾加上换行符

命令格式

printf format-string [argument]

printf "%10d!\n" 123 # 占10位, 右对齐
printf "%-10.2f!\n" 123.123123 #占10位, 保留2位小数, 左对齐
printf "My name is %s\n" "yogurt"
printf "%d * %d = %d \n" 2 3 `expr 2 \* 3`

test命令与判断符号[]

用于判断一个表达式是否为真

test命令是一个shell内置的命令,其与[]的作用几乎一模一样

逻辑运算符 &&||,这个是属于shell的。二者也遵循短路原则

  • exp1 && exp2:当exp1为假时,会直接忽略exp2
  • exp1 || exp2:当exp1为真时,会直接忽略exp2

当表达式的exit code为0时,表示真;非0表示假。(与C/C++中的定义相反)

test命令用于判断文件类型,以及对变量做比较

test命令用exit code返回结果,而不是用stdout

0表示真,非0表示假

test 2 -lt 3
echo $? # 得到0, 表示 2 < 3 的结果是真

test 3 -lt 2
echo $? # 得到1, 表示 3 < 2 的结果是假

文件类型判断

test -e filename,判断文件是否存在

参数

  • -e 文件是否存在
  • -f 是否为文件
  • -d 是否为目录
test -e test.sh && echo "exists" || echo "Not exists"

文件权限判断

test -r filename,判断文件是否可读

参数

  • -r 文件是否可读
  • -w 文件是否可写
  • -x 文件是否可执行
  • -s 文件是否非空

整数间比较

test $a -eq $b

参数

  • -eq 是否相等
  • -ne
  • -gt
  • -ge
  • -lt
  • -le

字符串比较

参数

  • test -z STRING 判断STRING是否为空
  • test -n STRING 判断STRING是否非空(-n可以省略)
  • test str1==str2 判断是否相等
  • test str1!=str2

多重条件判定

test -r filename -a -x filename

参数

  • -a 两个条件是否同时成立(a for and
  • -o 两个条件是否至少一个成立(o for or
  • ! 取反。如test ! -x filename

判断符号[]

[]test用法几乎一模一样,更常用于if语句中。另外[[]][]的加强版,支持的特性更多

例如

[ 2 -lt 3 ] # 为真, 返回值为0
echo $? # 输出0
[ -e test.sh ] && echo "exists" || echo "Not exists"

注意

  • []中的每一项之间都要用空格隔开,比如[ 2 -lt 3 ]
  • []括号内的变量,最好用双引号括起来
  • []括号内的常数,最好用单/双引号括起来

name="acwing yxc"
[ $name == "acwing yxc" ] # 错误, 等价于[ acwing yxc == "acwing yxc" ] 会报错参数过多
[ "$name" == "acwing yxc" ] # 正确
path=/data/dev

if [ -e "${path}" ] && [ -d "${path}" ]
then
  echo "${path} exists , and it's a folder, not a file"
fi
s1=abc
s2=abc/
s3=$(echo ${s2} | awk -F / '${print $1}') # 去掉s2后面的/

if [ "${s1}=${s2}" ]
then
   echo "two strings equals"
fi

type [ 会发现[是一个命令

判断语句

if…then 形式

单层if

命令格式

if condition
then
	语句1
	语句2
	...
fi

判断的是condition的退出状态(exit code)是否为0

所以condition应该是一个可执行的命令,if会根据这个命令执行的返回值(exit code)来判断条件是否成立

示例

a=3
b=4
if [ "$a" -lt "$b" ] && [ "$a" -gt 2 ]
then 
	echo ${a}在范围内
fi

单层if-else

if condition
then
	语句一
	...
else
	语句二
	...
fi

多层if-elif-elif-else

if condition
then
	语句一
	...
elif condition
then
	语句一
	...
elif condition
then
	语句一
	...
else
	语句一
	...
fi

case…esac形式

类似于java或C++代码中的swich

case $变量名称 in
	值1)
		语句1
		语句2
		...
		;;
	值2)
		语句1
		语句2
		...
		;;
	*)
		语句1
		语句2
		...
		;;
esac

示例

a=4

case $a in
	1)
		echo ${a}等于1
		;;
	2)
		echo ${a}等于2
		;;
	3)
		echo ${a}等于3
		;;
	*)
		echo ${a}等于其他
		;;
esac

循环语句

for循环

第一种for循环

命令格式

for var in val1 val2 val3
do
	语句1
	语句2
	...
done

示例:输出a b c 每个元素一行

for i in a b c
do
	echo $i
done

示例:输出当前路径下的所有文件名,每个文件名一行

for file in `ls`
do
	echo $file
done

若for循环是一个变量,则会以环境变量IFS作为分隔符,对变量进行分割;默认的IFS是换行符和空格。如果只需要按行读取,则可以先将IFS设置为换行符,如下

IFS="
"
for line in $(cat somefile.log)
do
	echo $line
done

示例:输出1-10

for i in $(seq 1 10)
do
	echo $i
done

或者使用{1..10}{a..z}

for i in {1..10}
do
	echo $i
done
for i in {a..z}
do
	echo $i
done

第二种for循环

类似于C++或者Java中的for循环

命令格式

for ((expression; condition; expresion))
do
	语句1
	语句2
	...
done

示例,输出1-10

for((i=1; i<=10; i++))
do
	echo $i
done

while循环

和C++与Java类似

命令格式

while condition
do
	语句1
	语句2
	...
done

示例,从键盘读入并输出数据,直到按下文件结束符(Ctrl + d)(当输入文件结束符EOF后,read命令返回false,此时循环结束)

while read name
do
	echo $name
done

另一种类似do-while循环的循环

命令格式

until condition
do
	语句1
	语句2
	...
done

只要condition为假,就一直执行,直到condition为真,结束。

示例

until [ "${word}" == "yes" ] || [ "${word}" == "YES" ]
do
	read -p "Please input yes/YES to stop this program: " word
done

break命令

跳出当前这一层循环。(注意shell中的break不能跳出case,与C++有点不一样)

continue命令

跳出当前这一次循环。

示例(输出1-10中的全部奇数):

for ((i=1; i<= 10; i++))
do
	if [ `expr $i % 2` -eq 0 ]
	then
		continue
	fi
	echo $i
done

死循环的处理方式:

  1. 使用top命令找到进程的PID
  2. 输入kill -9 PID 杀掉进程

函数

shell中的函数类似于C/C++的函数,但是return的返回值是exit code,取值为0-255,其中0表示正常结束。

如果想获取函数的输出结果,可以通过echo输出到stdout中,然后通过${function_name}来获取stdout中的结果。

函数的return值可以通过$?来获取

命令格式

[function] func_name() { #function关键字可省略
	语句1
	语句2
	...
}

示例

# 先定义一个函数
func() {
	name=hby
	echo "Hello $name"
	# 不写return语句,默认return 0
}
# 调用这个函数
func # 无需加小括号, 像命令一样直接调用

获取函数的执行结果和返回值

# 先定义一个函数
func() {
	name=hby
	echo "Hello, $name"
	return 123
}

output=$(func) #调用函数并获取stdout
ret=$? #获取返回值

echo "output=$output" # output=Hello, hby
echo "ret=$ret"  # ret=123

函数参数传递

在函数内,$1表示第一个参数,$2表示第二个参数,…

$0仍然是文件名,注意

示例(计算1到n的和)

func() {
	if [ $1 -le 0 ]
	then
		echo 0
		return 0
	fi
	
	sum=$(func $(expr $1 - 1))
	echo $(expr $sum + $1)
}

echo $(func 10)  # 55

函数内的局部变量

可以在函数内部定义局部变量,作用范围仅在当前函数内。可以在递归函数中定义局部变量,以防止进行递归调用函数时,变量之间相互影响。

命令格式:

local 变量名=变量值

例如:

#! /bin/bash

func() {
	local name=hby
	echo $name
}

func # 执行函数
echo $name # 该变量不存在,输出空串

其实shell执行的很慢,可以将上面的函数调用改为100,用time命令查看一下执行时间

time ./test.sh

会发现都需要0.5s,可见非常慢了

exit命令

exit命令用来退出当前的shell进程,并返回一个退出状态

exit命令可以接受一个整数值作为退出值(exit code),代表退出状态。如果不指定,默认是0

示例

#! /bin/bash
if [ $# -ne 1 ] # 如果传入的参数个数等于1
then
	echo "arguments not valid"
	exit 1
else
	echo "arguments valid"
	exit 0
fi

文件重定向

每个进程都会默认打开3个文件

  • stdin:标准输入,从命令行读取数据,文件描述符为0
  • stdout:标准输出,向命令行输出数据,文件描述符为1
  • stderr:标准错误输出,向命令行输出数据,文件描述符为2

可以用文件重定向,将上面3个文件重定向到其他文件中

重定向命令列表

  • command > file :将stdout重定向到file
  • command < file:将stdin重定向到file
  • command >> file:将stdout以追加方式重定向到file
  • command n> file:将文件描述符n重定向到file
  • command n>> file:将文件描述符n以追加方式重定向到file

示例

#! /bin/bash

echo -e "Hello \c" > output.txt
echo "World" >> output.txt

read str < output.txt # 将这条命令的stdin重定向到output.txt,而read命令是从stdin中读取数据,所以就变成了read从output.txt中读取数据
echo $str

可以同时重定向stdinstdout

先写一个脚本test.sh,从stdin中读入ab,并输出两数之和到stdout

#! /bin/bash

read a
read b

echo `expr $a + $b`

在编写一个input.txt

11
22

然后运行test.sh,将其输入重定向为input.txt

./test.sh < input.txt # 结果 33
./test.sh < input.txt > output.txt # 同时重定向输入和输出
# 等价于
./test.sh 0< input.txt > 1output.txt # 同时重定向输入和输出

引入外部脚本

C++中可以用include引入外部文件,java可以用import引入

shell中也可以引入其他文件中的代码

语法格式如下

. filename # 注意点号和文件名之间有一个空格

# 或者

source filename

示例

先创建一个文件test1.sh,内容如下

name=hby

再创建一个文件test2.sh,内容如下

#! /bin/bash
. test1.sh # 这里其实相当于把 test1.sh里面的内容全部拷贝到这里了
echo $name

甚至test1.sh都不需要是一个.sh文件,任意文件都可。

比如其文件名为data(不带任何扩展名)

然后在test2.sh中直接引入这个data文件

#! /bin/bash
source data
echo $name

并且引入文件,可以加路径,用相对路径或者绝对路径都可

注:

因为我们的命令行终端,也是一个bash程序,可以将其理解为一个大的文件。

这个bash程序执行之前,会先执行一次.bashrc里面的所有内容。这个.bashrc里面会设置一些环境变量,命令别名什么的。

我们修改完.bashrc这个文件后,一般都会source一下

source .bashrc

这个source .bashrc的命令,就是将.bashrc里的内容全部执行一遍

但是像vim有个.vimrc,这个文件用source的话会报错。

因为.vimrc它不是一个shell脚本,而是vim自带的一些语法。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值