Linux--Shell
Shell 创建与使用新命令
步骤:
- 将脚本保存到文件中
- 赋予文件执行权
chmod +x file
- 将文件放到$PATH目录下(一般都是保存到\home\用户名\bin下)
当然我们也可以直接如下
这种方法每一次都要写./ 不同方便
上述步骤方法可以让我们直接使用 nu 这种写
书写Shell脚本
#! 的作用非常大,他可以帮助我们在执行脚本的时候不用每次都写解释执行脚本的程序
如awk -f 可以省略,在我们写了#!/usr/bin/awk -f之后
命令执行的整个流程
参数处理
当我们写Shell脚本的时候如何处理参数呢?
$*
比如我们就写了个脚本MYLS,其中的内容就是ls -l
但是ls -l 后可以接多个参数 file1,file2,....
当我们也想我们的脚本实现MYLS file1 file2...咋办?
可以脚本中写 ls -l * 可以代表全部参数(不包括@作用基本与$*相同
$0
$0在shell脚本中代表shell文件名
需要注意的是这个文件名是全名称的,即:
如果就只想要活动文件名,可以试试basename命令
我们还可以使用2....{10} 表示第10个参数
$# 表示总的参数个数
shift[n] 命令可以对参数进行左移,一般是写在脚本中,n是参数
如
echo $# echo $1 shift 2 echo $1
上述操作后第一次打印的是参数个数,然后打印第一个参数,然后将全部参数左移2个,然后打印第一个参数(这个时候第一个参数其实是原理的第三个参数)
命令替换
思考一下如下问题:
我们如何将一个命令的运行结果作为参数呢?
即可以使用`` 的方式,但是在``中再希望使用`` 需要加上
更好的方法是使用$()
变量
我们在shell中可以直接定义变量
需要注意的是 x=100 之间是不能有任何空格的,否则100 会被当做参数,x会被当做命令来处理,结果就是
还需要注意的是我们在shell定义的变量默认保存的是string值
y=6/3 echo $y
结果不是2,而是6/3
作用域
当然,我们的变量也是有作用域的
一般在父Shell下定义的变量是不能够在子shell中使用到的
解决方法 export x (x为变量)
一般我们是在子Shell中改动父shell中定义的变量后,返回父Shell是不影响父Shell的 (比如我们写脚本对父Shell进行更改是无用的,因为脚本其实是原来Shell的子Shell)
解决方法: 我们可以使用". 脚本" 的方法在父shell中执行这个脚本
(source 脚本效果也是一样的)
这个其实是有经常用到的地方,如:我们希望我们改过配置文件后,不用重启操作系统就可以立刻更新
删除变量、声明变量和特殊变量
x=100 unset x
被声明为只读的变量无法删除(declare -r x)
help declare
查看declare 声明变量的帮助
我们知道我们的变量默认是string类型的,我们可以
declare -i x x=6/3
这次结果为2
进制
特殊变量
变量替换、条件变量替换
思考一下如下要求:
a=teach # 我想要打印出I'm a teacher如何办到?
这样吗?echo "I'm a $aer"
不,错误的,他会把$aer当成变量
答案是echo "I'm a ${a}er"
需要注意的是这里的双引号不能改为单引号,因为单引号是强引用,会将其中全部的东西当做字符串
()是命令替换 注意区别
${!y} !会递归地将最初始的值返回出来:
条件变量替换
比如:
这个命令的作用是如果1
否则ls -l . 这个.表示当前目录
截取变量(有点substr使用的感觉?)
varname="game.tar.gz" echo ${varname%.*} echo ${varname%%.*}
需要注意的是这个这种是文件通配符的中的含义
varname%.* 表示去除varname中与.* 最短匹配上的内容
echo ${#varname} //输出变量长度 echo ${varname:0:3} //取变量varname从第0个字符开始,3个字符、 echo ${varname::3} //省略开始字符默认从0开始 echo ${varname:-1:} //-1 表示倒数第一个字符
字符串匹配
value="This is a test and the test is hard" echo ${value/test/Test} //将匹配到的test单词替换为Test,匹配到后只能替换一次 echo ${value//test/Test} //全局替换 echo ${value/#test/Test} //与最左边单词进行匹配,匹配上了则替换,否则不替换 ehco ${value/%test/Test} //与最右边单词进行匹配,匹配上了则替换,否则不替换 echo ${value//test/} //表示删除test
shell中字符串匹配功能还是比正则弱不少的
数值计算
只能进行整数运算
- 内部命令: let 和 declare
declare -i x
让变量为int型没啥好说的
help let | less
- 外部命令
man expr
这里其实可以看到内部命令与外部命令的区别
内部命令如let 可以与shell本身的一些内部实现进行一些交互
比如可以直接使用let a=1 b=2 c=a+b 这样的
但是外部命令 expr 1 + 2 这里因为是将1,+,2当做参数,所以他们之间必须要有空格
外部命令是接受参数进行执行
- 算数扩展
$[] 效果好像也是一样的
可进行小数运算的bc
man bc
注释
单行注释 #
打印
printf "%d %s %d\n" $a $b $c
条件语句
echo $?
通过上述命令我们可以知道上一条写的条件语句是真还是假
真为0,假为1
test "$str" = "a b" # 上述 test 是 用来判断变量$str与字符串“a b”是否相同的 # 为何 $str 要用“”包住?因为$str其实相当于c中的# ,其处理的方式是宏替换 # 即上述语句其实写的是 test a b = "a b" # 这样写肯定不行 [ "str" = "a b" ] # [] 与 test有同样效果
这下不得不说说 [] 了
help test
查看帮助文档
可以看到[[]] 中是可以支持 文件扩展符的
其他一些可以在help test中看到
: 永远返回0
总结一下
我们如果想要像c++ 中有数值计算那么就用 $(()) 进行计算
如果算完后想要判断,那么用[ xxx -eq ] 这样的方式进行判断
数值千万不要用 == 等进行比较,判断! == 是判断字符串用的
流程控制
if [ xxx ]; then xxx elif [ xxx ]; then xxx else xxx fi case xxx in xxx) dosomething;; xxx) dosomething;; ... esac while [ xxx ]; do xxx xxx done for i in `seq 10`; do echo $i done 还有用法是: for ((i=1;i<=10;i++)); do echo $i done # 上述这条shell命令会打印出1~10的数值
同样也有 break 和 continue
用 \ 进行代码换行
关于for 循环的参数问题
for i in * ; do echo $i done for i in $* ; do echo $i done
- 表示当前执行文件夹的全部文件,这里可以支持文件扩展符
$* 就表示全部的参数
for i in "$@"; do echo $i done
上述这种写法将全部的参数当做字符串,这样做可以处理参数是字符串时不会出现奇奇怪怪的问题
参数的分隔依据
可以看到,他这里读参应该是根据 空格 \n \t 进行分隔
其实他其实是参照环境变量 $IFS 当做分隔符的
当然我们可以改动:
我们在原来的$IFS上加上了 ,与 :当做分隔符
用户交互
read
read 是相当于c++ 中的 cin
通过 help read
查看帮助信息
-p 可以 在输入时添加提示
-s 是不显示输入的内容
select
函数
函数的书写方式与调用
函数中参数的使用
在上面函数的调用方法中我们可以看到 我们就像普通调用命令一样,调用函数,然后参数追加到函数后面
在函数中使用参数依然是用@等
函数中全局变量和局部变量
可以看到我们在函数中定义的变量也是全局变量
为了定义局部变量需要再变量前加上 local 字段
注意! 使用 管道时 变量的范围的问题
为何a不等于“5678”呢?
在 man bash中的 pipelines下
即在管道中执行的命令都是开了一个子shell进行运行的,其中的变量并不是我在父shell中定义的
再如:
这个n肯定我们执行后是0,咋解决呢?
不用管道了!
这用法真新奇
函数的返回值
我们可以通过 $? 来获取 函数的用return返回的 返回值
但是这是有范围的
所以一般我们通过接受函数的输出值来得到函数的返回值
我们可以通过result=$(functionName)
这时 result就得到了函数中输出的值
我们来分析一下上述程序
首先,我们输出“Enter a value:\c”
这是提示
echo -e echo加上-e选择后可以解析一下特殊字符
这里的\c是为了echo 不自动换行
然后我们将标准输定向到标准错误输出,即为了防止这个提示语句在我们调用函数后被接受到
echo 后是我们要输出的结果
result用于接收
函数库
需要注意下这里 error $msg 的写法,这种写法可以让字体变色,即更像警告一样了
数组
数组的基础使用
- 定义数组
declare -a arrName 或 arrName=("xx" "xx" "xx"...) declare -a #可查看定义的数组
- 访问数组
# 数组下标从0开始 echo ${arrName[3]} # 打印数组全部内容 echo ${arrName[*]} 或 echo ${arrName[@]} # 打印数组长度 echo ${#arrName[*]} # 从数组下标3开始,取3个值 echo ${a[*]:3:3}
- 数组的变量替换
数值传值
- 将read内容保存到数组中
xxx | while IFS=":" read -a arrName ; do xxx done
- 将数值作为参数传递给函数
即 我们首先需要将数组中的内容通过${arrName[@]}展开
result={arrName[@]})
- 函数返回数组
function functionName(){
xxx
echo ${arrName[@]}
}
我们通过(functionName))
将数据与脚本放在一起 -- Here文档
xxx<<HereName xxx xxx ... 数据 xxx HereName HereName是随意的
here文档中忽略特殊字符问题
在here文档中 $(),${}等依旧是有效的,可以进行替换操作 如果不想要这种特殊替换的话可以在前面加上\ 即\$() 如果想要全局禁止特殊替换可以在HereName前加上\ 即 xxx <<\HereName ... HereName
here文档中忽视数据存放时Tab格式问题
如果没有这个- 那么我们输出到$1的内容将会保留我们写在HTMLSKEL之间的格式
利用here文档实现shell中多行注释
: <<Explanatory_note xxx xxx ... xxx Explanatory_note
here文档实际运用
实现将多个文件打包并解包的功能
实现这个功能的脚本为bundle
bundel file1 file2 >> allfile 这条命令是将file1,file2打包进allfile
sh allfile 这条命令可以将allfile解包,回复fil1,file2
其实实现的逻辑很简单,通过$* 遍历每一个参数文件,然后利用cat i <<'End of i"形成here文档的形式,将打印出来的文件内容包裹起来,而且将脚本写入allfile中
这个时候allfile其实就是这个包含了数据的脚本,然后通过执行这个脚本可恢复源文件
exec
作用一:执行新进程并用新进程替代当前进程
uname -r exec date echo 'exec failed!'
exec 执行后当前shell被替换内容被替换,exec下面的全部命令也就执行不了了
作用二: 重定向
打开sh.out写,并给它分配文件描述符4
exec 4>sh.out
把输出文件描述符4复制到1,即文件描述符1指向与文件描述符4一样的地方
exec 1>&4
关闭文件描述符4
exec 4>&-
将标准输出重定向到/dev/tty(即当前的终端)
exec >/dev/tty
实践
我现在想要从file1和file2文件逐行读入
下面这么写对吗?
不对! 这样写read line1<$file1相当于不断打开file1进行读,这样永远会只读第一行
我们这里的做法是将file1打开,并赋予文件描述符3
然后将read的标准输入指向文件描述符3,这样就避免了重复打开file1的问题
进程替换
我们来理解一下:
说明 <()操作其实取的是执行()中命令 对应的/dev/fn/n文件
我们首先打印出了 ls 对应的/dev/fn/n文件,然后ls执行了,所以就有上图的结果
再如:
我们打印出"Hello" 然后通过 > 给文件/dev/fn/n ,然后cat /dev/fn/n
管道与进程替换的区别
在上述博客中提到了:
命名管道
命名管道是个文件,普通管道是个程序
mkfifo fifoName
ls -l fifo
最前面的 p 表示其是一个文件管道
一些内部命令
命令分为内部命令和外部命令,内部命令本身就是Shell,执行内部命令是在当前Shell执行
然后执行外部命令需要,需要创建子Shell中执行
set
-
set 命令主要用来设置shell,在编写shell脚本时,使用 set 命令能设置shell的执行方式,根据需求不同,采用的参数设置也不同。set 命令也用来显示系统中已存在的shell变量以及设置新的shell变量。
参考博客 -
set 可以重置shell中的参数
这个用法可以扩展出很多小技巧
通过set改变参数,再通过$#得到全部参数个数
eval
eval会对后面的命令进行两遍扫描,
如果第一遍扫描后,命令是个普通命令,则执行此命令;
如果命令中含有变量的间接引用,则保证间接引用的语义。也就是说,eval命令将会首先扫描命令行进行所有的置换,然后再执行该命令。
因此,eval命令适用于那些一次扫描无法实现其功能的变量。
- eval 执行以下两个步骤:
- 第一步,执行变量替换,类似与C语言的宏替代;
- 第二步,执行替换后的命令串。
不使用eval,执行出错:
反之:
信号
man 7 signal
可以查看更详细信息
trap 可以用于捕获信号并进行处理
当Shell是因为信号而停止时,$?的值为128+信号的值
然后为了不让每一次按ctrl+c都出现 I get 130,我们需要使用
trap - 2
结合函数使用
我们可以看到trap 后面的“”中调用了函数
Shell选项处理
简单选项处理
shift 默认执行的是 shift 1
表示将参数整体向左移一位,即可以得到将原来的1 效果
具体博客<---
我们这里利用shift不断让参数到达$1这个位置
- 利用 -- 分离选项和参数
-- 之后就是参数了
合并选项处理
如 : -ac
上面如:
case "$1" in -a) -b) -c) *)
我们是有对a,c选择的处理
-a -c 分开了写就行
但是-ac合起来写,我们上面的简单处理就会报错
getopt
getopt ab:cd -a -b test1 -cd test2 test3 ab:cd : 表示b后面必须要有参数
然后我们的 getopt 会将合并的参数给自动拆开了,而且参数后没有写":" 的会被认为没有参数,上述代码执行结果为:
- 需要注意的是 getopt对“”中内容的处理
可以看到其中""中的内容并没有被当做一个参数,而是分开来了
有个问题就是选项后只接受一个参数,多了参数会有奇怪的问题
处理“”中参数问题的getopts
在选项前面加“:”是为了忽略错误,在后面加“:”是为了说明其后有参数
上述代码中opt保存了选项后的参数
执行这个代码:
有与getopt一样的问题:
如果是选项后出现多个参数会出现奇怪的问题
正常效果:
如红框这么做,可以得到非选项的参数
(())效果一样
终端控制
颜色设置
我们可以通过在输出内容前写 ESC[
来控制输出颜色
\033[ \e[ 也代表ESC[ ESC[arg1;arg2;...m m为结束
利用 man console_codes 来查看书写和颜色编号
echo -e "\e[4mhello world\e[0m" 前者\e是为了表示hello world是用特殊书写 后者\e是为了恢复常态 -e 是为了忽视特殊字符
终端控制 tput
man tput :
后面的参数我们可以通过 man 5 terminfo 查看:
我们经常有些会连用,比如:
tput sc # 先保存光标的位置 xxx tput cpu r c #将光标移动到指定位置进行一些操作 xxx tput rc #恢复光标的位置
tput setf 1 tput setb 2 xxx tput sgr0 #将上述设置的一切给取消,下面接着使用默认值
运用
结合命令替换使用
这里有个惊人的使用方法,我们知道 `` 是用来命令替换的 , 这里将${}展开命令替换操作
然后可以通过这个方式让这个输出的hello有特殊样式
结合Here文档使用
效果如下:
制作选择菜单
效果如下:
调试
shell的-v和-x选项
比如我下面的一段测试脚本:
不开启调试:
开启-v调试:
可以看到-v只是将原始的句子打印出来而已
对应更复杂的有循环语句,他也只是打印出来原始句子
开启-x调试:
可以看到+x会把过程中的变量等信息展示出来
对应更复杂有循环的语句会逐步进入展示
在脚本中开启和关闭调试
在脚本中通过 -x/-v 开启调试
然后下面的代码都是开启调试的状态
再通过+x/+v 关闭调试状态
利用trap和伪信号进行调试
利用trap命令进行调试
伪信号:
由shell而非内核产生的信号
EXIT/0 从函数或脚本中退出
RETURN 调用函数或使用“.”命令执行其他脚本之后
ERR 从某条命令返回非0状态(不成功)时
DEBUG 脚本中每条命令执行之前
可以用于当我们要退出Shell脚本的时候,可以利用trap "" EXIT
察觉到脚本要退出了,在“”中可以执行一些清除这个脚本中产生的中间文件的操作
set -o functrace 记得写上
利用debug钩子进行调试(即就是写debug函数,并通过变量的值来灵活调试)
可以看到,我们debug()函数中通过DEBUG变量来判断是否进行debug
在调用这里脚本时,可先对DEBUG赋值来决定是否需要调试
通过()和 {} 设置在当前Shell执行还是在子Shell下执行
可以看到上述案例,我们将cd 这条命令在子shell下运行,这样我们执行完后的Shell并不会到 cd的目录下
我们在子Shell下运行的并不会对本Shell中变量造成影响
但是当我们用{}时:
注意{}中以 “;”进行分隔,而且最后的语句也要用;结尾
还可以有上面的写法
实现Shell脚本中将参数给awk
这个程序通过管道,将who的输出连接到了shell脚本testawk的输入,因为awk就是从标准输入中读数据,所以awk能够接受到数据
注意我们这里的同样可以给testawk 传参数
然后1被替换后(假设传入的为3),那么$3就是awk要处理的第3个字段
'{ print 1为一部分; ' }'为一部分
通过这三部分组成了awk要执行的程序
综合大作业:实现就业管理系统
先思考下下面这个页面咋做?
# 首页面,对system进行讲解: 40 WelcomeIndex(){ 41 42 sr=5 43 44 tput clear 45 center $sr "Welcome to Employment System" 46 titleFont "Welcome yo Employment System!" 47 sr=$[sr+2] 48 49 50 center $sr "The author is ${author}" 51 echo "The author is ${author}" 52 sr=$[sr+2] 53 54 center $sr "Now the system version is ${version}" 55 echo "Now the system version is ${version}" 56 sr=$[sr+2] 57 58 tput cup $sr 5 59 echo "Description:" 60 sr=$[sr+2] 61 62 tput cup $sr 7 63 echo "The main function of this employment system is to manage a large number of informations about employment situation of college students." 64 sr=$[sr+3] 65 66 67 tput cup $sr 7 68 echo "You can use this system to accomplish a series of functions included Add,Delete,Alter,Find." 69 sr=$[sr+3] 70 71 tput cup $sr 7 72 echo "Good Luck!" 73 sr=$[sr+2] 74 75 center $sr "Now you can press Any Key to continue!" 76 titleFont "Now you can press Any Key to continue!" 77 78 tput sgr0 79 80 read -n1 key 81 }
一些初始时需要设定的函数和参数
1 #!/bin/bash 2 3 # 得到当前窗口的大小 4 pwidth=$(tput cols) 5 phight=$(tput lines) 6 author="次林梦叶" 7 version="0.1" 8 9 #程序在执行过程中会产生中间文件,需要在程序退出的时候清除 10 11 #trap 'rm -rf ./tmp &>/dev/null; tput sgr0;' EXIT 12 13 14 15 # 输入行数,自动将文字打印到屏幕中间 16 center(){ 17 tput cup $1 $(((pwidth-${#2})/2)) 18 } 19 20 # 输入时用的格式,比center更加往左边一点 21 leftCenter(){ 22 tput cup $1 $(((pwidth-${#2})/4)) 23 } 24 25 # 专门用来打印标题文字格式 26 titleFont(){ 27 tput bold 28 echo -e '\033[42m'$1'\033[49m' 29 tput sgr0 30 } 31 # 专门用来打印选择文字格式 32 selectFont(){ 33 echo -e '\033[4;33m'$1'\033[49m' 34 } 35 # 专门用来打印错误文字格式 36 errorFont(){ 37 echo -e '\033[41m'$1'\033[49m' 38 }
我自认为写的不错的一些方法
291 #修改的基本想法是,通过cut得到id,然后与employment.db中的每一个id进行比较 292 #不同的就放到临时文件中,因为这说明这条信息是不用更改的,最后将临时文件中的内容代替employment.db 293 flag="fail" 294 touch ./tmp/altTemp 295 while read line ; do 296 field1=$( echo $line | cut -d'%' -f1 ) 297 298 if [ "$field1" != "$key" ] ; then 299 echo $line >> ./tmp/altTemp 300 else 301 flag="success" 302 alterLine=$line 303 fi 304 done < ./employment.db 305 306 cat ./tmp/altTemp > ./employment.db 307 rm ./tmp/altTemp 308 309 if [ $flag == "fail" ]; then 310 center $sr "Error! Can't find field what you enter!" 311 errorFont "Error! Can't find field what you enter!" 312 sr=$[sr+2]
本文作者:次林梦叶
本文链接:https://www.cnblogs.com/cilinmengye/p/17788970.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步