Shell类型
shell种类特别的多,一般在Linux下默认都是bash,我们可以通过cat /etc/passwd
查看每个用户默认的shell。同时我们在工作中可能会用到zsh,其实zsh是兼容bash的。
一般在shell下执行的命令有两种:
- 内建命令
- 非内建命令
内建命令在执行时一般不会fork出子shell,内建命令有cd、alias、exit
,一般我们通过which cmd
可以查出命令是不是内建命令。不管是内建命令,还是非内建命令,我们都可以通过$?
来获取命令的返回码(返回吗一般是0表示成功,非0表示失败)。
Shell如何执行命令
执行命令我们一般是分单条执行还是多条执行。
单条执行
其实就是我们常用的方式,只不过是如果是内建命令就不会fork而已,非内建命令会fork/exec/wait
。
多条执行
其实就是我们说写的脚本(批处理),把多条命令放到一个文件中,然后一块执行。
比如有如下一个脚本,文件名为:script.sh
:
1 | bash复制代码#! /bin/sh |
一般有两种执行方式:
- 以一种可执行文件的方式来执行。
chmod +x script.sh
然后./script.sh
就可以执行了。这种执行方式的大概流程为:先fork出一个子shell,在子shell执行通过exec来执行该脚本中第一行所指定的/bin/sh
的解释器(相当于将sh
的代码段加载到这个子进程中来),而当前script.sh
是作为一个命令行参数来传递给sh
的。当执行到cd ..
,由于cd
是内建命令,相当于sh
内部调用一个函数来改变当前子shell的工作路径;然后再执行ls
,由于ls
不是内建命令,所以会再fork
出一个子进程执行ls
,子shell调用wait
等待ls
进程执行完,当ls
的进程执行完之后,子shell的wait
会解除,由于脚本中的代码已经执行完了,所以主shell中wait
子shell也解除,因此我们就能看到shell提示符了。
- 直接通过解释器来执行
也就是这种sh ./script.sh
方式。我们会发现,其实两种方式的本质还是一样的。
我们应该能发现在脚本中有一句cd ..
,但是脚本执行完之后,我们的工作目录本没有任何改变,这是为啥?这是因为修改的是子shell的工作目录,当前主shell的目录并没有修改呀。 - 执行脚本时能不能先不要fork出子shell
答案肯定是可以的。比如我们执行以下的方式source ./script.sh
或者. ./script.sh
,这种方式就不会先fork出子shell来执行,但是到单个命令时还是会fork。这里可以联想到我们平时在定义环境变量时脚本,我们执行该脚本时都用source script.sh
的原因。因为执行不会fork,所以环境变量就是针对当前的shell进程的;如果执行之前的方式执行,仅仅修改的子shell的环境变量,主shell环境变量并没有任何变化。
另外要注意,通过(cd ..; ls -l)
这样也是会fork的。
Shell基本语法
变量
在shell中变量分成本地变量、环境变量。本地变量只能在当前的shell进程中使用,而环境变量可以在当前的shell进程的子进程中使用。
- 本地变量
本地变量的定义方式:
- 本地变量
1 | ini复制代码VALNAME=value |
等号两边不能有空格。获取获取变量的值呢:
1 | bash复制代码echo ${VALNAME} |
在shell中所有变量的类型都是字符串类型,如果变量没有定义就是空字符串。
- 环境变量
将本地变量导出就是环境变量,可以通过命令export VALNAME
。也可以一边定义一边导出:
- 环境变量
1 | ini复制代码export AAA=value |
查看环境变量,可以通过env
或者printenv
。环境变量可以有父进程传递给子进程。
- 如何使用变量
使用变量推荐使用:
- 如何使用变量
1 | bash复制代码echo ${SHELL} |
也就是大括号的方式。这样的好处就是后边跟一些字符串,变量展开以后也是可以拼接上的。比如echo ${SHELL}abc
,那么结果就是/bin/zshabc
- 变量的类型
在shell里边可以通过declare
来声明变量,例如如下代码:
- 变量的类型
1 | bash复制代码#!/bin/bash |
同时我们也可以通过declare
来声明数组
1 | bash复制代码#!/bin/bash |
- 变量内容的删除与替换
变量内容的删除,最基本的语法为${var#模式}
/${var##模式}
/${var%模式}
/${var%%模式}
/${var/old/new}
/${var//old/new}
- 变量内容的删除与替换
#
表示从前往后删除,删除满足要求最短的字符串
1 | bash复制代码file_path="/home/test/workspace/shell/test.cpp" |
##
表示从前往后删除,删除满足要求的最长字符串
1 | ini复制代码file_path="/home/test/workspace/shell/test.cpp" |
%
表示从后往前删除,删除满足要求的最短字符串
1 | bash复制代码file_path="/home/test/workspace/shell/test/test.cpp" |
%%
表示从后往前删除,满足要求的最长字符串
1 | bash复制代码file_path="/home/test/workspace/shell/test/test.cpp" |
- 使用
${var/old/new}
替换时,仅仅替换一次。使用${var//old/new}
替换时,是替换全部
1 | bash复制代码file_path="/home/test/workspace/shell/test/test.cpp" |
- 变量的测试
这里的内容还挺多的,我这里仅仅记录我常用的。new_var=${old-content}
表示old
没有定义时,new_var
使用content;如果old
定义了并且是个空字符串,那么new_var
也是""
。另外new_var=${old:-content}
,也就是说old
没有定义还是个空字符串,那么new_var
就取content,否则就取old
。
- 变量的测试
文件名代换globbing
其实就是一些通配符。
通配符 | 作用 |
---|---|
* | 匹配0个或者多个任何字符 |
? | 匹配1个任意字符 |
[] | 匹配方括号中其中一个字符 |
比如我们执行ls ch1[0-2].doc ,那么shell其实会先展开通配符,比如能找到ch10.doc 、ch11.doc ,然后将这两个文件传递给ls。也就是说ls并不会不处理这些通配符,是由shell先展开再传递给ls。 |
命令行代换``、$()
通过``或者$()括起来的也是一条命令,shell会先执行这条命令,将执行结果放到当前所在的命令行中。
1 | bash复制代码DATE=`date` |
算术代换:$(())
shell中变量默认都是字符串,所以可以涉及到整形变量+
、-
、*
、/
可以这样处理:
1 | bash复制代码val=10 |
$(())
只能用于整形运算
转义字符\
有一些特殊的字符如果我们想使用这些特殊字符的字面量时,就可以使用转义。echo \$SHELL
结果就不打印SHELL变量了,而是打印$SHELL
。
另外\
也可以表示续行的意思。
单引号、双引号
单引号表示字符的字面值。双引号一般情况下都表示字符的字面值,但是在遇到$变量名
则会展开变量值;在遇到``则会命令替换。
bash启动脚本
就是bash启动时执行的脚本,这里边的规则还是挺复杂的,但是只需要记住一条,我们可以将环境变量、alias、mask定义到.bashrc文件中即可。这样在bash启动时会自动source启动脚本,这样我们预先定义的变量就自动生效了。
shell脚本语法
条件测试
条件测试语句在shell中有两种表达[]
/[[]]
/test
,条件测试语句可以测试字符串/数值/文件的属性。比如
1 | perl复制代码VAR=2 |
不管用那种方式,最终表示条件是真还是假,是通过$?
来判断的,0表示true,1表示false。
对于与或非
在shell中[[]]
是[]
的拓展,并且[[]]
是兼容[]
的。竟然是拓展,那功能肯定要比之前的[]
要强一些的。
- 替换掉
[]
中的-a
或者-o
1 | lua复制代码[ -f README.md -a -x README.md ] && echo "yes" || echo "no" |
- 使用正则表达式
1 | lua复制代码A="hello"; [[ "$A" =~ hell? ]] && echo "yes" || echo "no" |
判断语句
在shell编程中是可以使用if
语句的,但是跟我们的c语言的if也不太一样。
1 | bash复制代码if [ -f ~/.bashrc ]; then |
其实涉及三条语句,if [ -f ~/.bashrc ]
如果测试语句$?为0则表示为true,否则为false。then . ~/.bashrc
在shell一般一行只有一条语句,如果要有多条语句的话,就用;分离。fi
表示if的结束标记。
特殊if语句,也就是if的条件永远为true。:
是空语句,执行结果$?是0。
1 | bash复制代码if :; then echo "always true"; fi |
常见的if
用法为:
1 | bash复制代码num=1 |
&&
、||
用法,其实就是利用短路特性。
1 | bash复制代码test "$(whoami)" != 'root' && (echo you are using a non-privileged account; exit 1) |
对于if
语句最佳实践,最好对于if [ $var xxxx ]
中的$var
用""
括起来。
case语句
case语句就是c语言中的switch case。大概的用法是这样:
1 | bash复制代码case $1 in |
以case开头,在in里边进行匹配,支持通配符,一旦找到某一个条件就执行对应的语句最终语句是以;;
结束。整个语句是以esac结束。
for/do/done
for循环。比如
1 | bash复制代码for FRUIT in apple banana pear; do |
如果想将某一个目录下的文件改名,可以这样:
1 | bash复制代码files=`ls test` |
也可以写类似于c语言的循环形式
1 | ini复制代码declare -i mi |
while/do/done
1 | bash复制代码COUNTER=1 |
这个应该很好理解。
位置参数与特殊变量
可以参考如下列表:
其中位置参数可以用shift命令左移。比如shift 3表示原来的4现在变成4现在变成4现在变成1,原来的5现在变成5现在变成5现在变成2等等,原来的1、1、1、2、3丢弃,3丢弃,3丢弃,0不移动。不带参数的shift命令相当于shift 1。shift可以使用场景就是在脚本内部可能会调用其他的脚本,但是参数又不想用那么多,这个时候就可以使用shift进行丢弃。
函数
shell中的函数不用写参数列表跟返回值类型的。是可以传递参数跟返回值的,只不过不需要声明而已。
1 | bash复制代码is_directory() |
在shell脚本中如果要表示返回true,一般用return 0,这个跟if
判断是有关系的。另外在shell空语句用:
来表达。
常见技巧积累
如果脚本遇到错误,最好能暴露出来,不要埋雷
这种情况下,我们一般要不使用set -e
或者是指定脚本解释器时,指定-e
参数,比如这样:
1 | bash复制代码#!/bin/bash -e |
这样脚本一旦遇到error就会停止运行,并且$?
也会返回非0
显性指定脚本的工作目录
也就是说,不管使用者从那个位置启动脚本,该脚本都能一如既往的按照预期的目录工作。一般情况下我们将某个脚本放置在某个目录,那一般工作路径基本上是基于该目录的,所以我们最好再写脚本时,在脚本的最开头这样写:
1 | bash复制代码dir=$(dirname $(readlink -f "$0")) |
这种写法也同时考虑了软连接的情况
脚本的命令行参数处理技巧
最简单的处理,应该是一个for再加一个case语句来处理,复杂点的参数可以通过getopt
来实现。
1 | bash复制代码#!/bin/bash |
其他
|
命令的右边必须是能接受标准输入作为参数时才能正常运行。比如echo "helloworld" | echo
就不会有任何输出,可以通过xargs
命令将标准输入转成命令行参数, echo "helloworld | xargs echo
就可以正常打印了
本文转载自: 掘金