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
:
#! /bin/sh
cd ..
ls
一般有两种执行方式:
- 以一种可执行文件的方式来执行。
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进程的子进程中使用。
-
- 本地变量 本地变量的定义方式:
VALNAME=value
等号两边不能有空格。获取获取变量的值呢:
echo ${VALNAME}
在shell中所有变量的类型都是字符串类型,如果变量没有定义就是空字符串。
-
- 环境变量
将本地变量导出就是环境变量,可以通过命令
export VALNAME
。也可以一边定义一边导出:
- 环境变量
将本地变量导出就是环境变量,可以通过命令
export AAA=value
查看环境变量,可以通过env
或者printenv
。环境变量可以有父进程传递给子进程。
-
- 如何使用变量 使用变量推荐使用:
echo ${SHELL}
也就是大括号的方式。这样的好处就是后边跟一些字符串,变量展开以后也是可以拼接上的。比如echo ${SHELL}abc
,那么结果就是/bin/zshabc
-
- 变量的类型
在shell里边可以通过
declare
来声明变量,例如如下代码:
- 变量的类型
在shell里边可以通过
#!/bin/bash
declare -i mi
declare -i mx=100
declare -i s=0
for((mi=0; mi <= mx; mi=mi+1)); do
s=s+mi
done
echo $s
同时我们也可以通过declare
来声明数组
#!/bin/bash
declare -a colors
colors[0]="yellow"
colors[1]="white"
colors[2]="black"
echo "len=${#colors[@]}"
for co in ${colors[@]}; do
echo $co
done
-
- 变量内容的删除与替换
变量内容的删除,最基本的语法为
${var#模式}
/${var##模式}
/${var%模式}
/${var%%模式}
/${var/old/new}
/${var//old/new}
- 变量内容的删除与替换
变量内容的删除,最基本的语法为
#
表示从前往后删除,删除满足要求最短的字符串
file_path="/home/test/workspace/shell/test.cpp"
echo ${file_path#/*/}
## 输出内容为:
test/workspace/shell/test.cpp
##
表示从前往后删除,删除满足要求的最长字符串
file_path="/home/test/workspace/shell/test.cpp"
echo ${file_path##/*/}
## 输出内容为:
test.cpp
%
表示从后往前删除,删除满足要求的最短字符串
file_path="/home/test/workspace/shell/test/test.cpp"
echo ${file_path%/test*}
## 输出内容为:
/home/test/workspace/shell/test
%%
表示从后往前删除,满足要求的最长字符串
file_path="/home/test/workspace/shell/test/test.cpp"
echo ${file_path%%/test*}
## 输出结果为:
/home
- 使用
${var/old/new}
替换时,仅仅替换一次。使用${var//old/new}
替换时,是替换全部
file_path="/home/test/workspace/shell/test/test.cpp"
echo ${file_path/test/learn}
# 输出:
/home/learn/workspace/shell/test/test.cpp
echo ${file_path//test/learn}
# 输出
/home/learn/workspace/shell/learn/learn.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会先执行这条命令,将执行结果放到当前所在的命令行中。
DATE=`date`
echo $DATE
或者
DATE=$(date)
echo $DATE
算术代换:$(())
shell中变量默认都是字符串,所以可以涉及到整形变量+
、-
、*
、/
可以这样处理:
val=10
echo $(($val + 10))
$(())
只能用于整形运算
转义字符\
有一些特殊的字符如果我们想使用这些特殊字符的字面量时,就可以使用转义。
echo \$SHELL
结果就不打印SHELL变量了,而是打印$SHELL
。
另外\
也可以表示续行的意思。
单引号、双引号
单引号表示字符的字面值。双引号一般情况下都表示字符的字面值,但是在遇到$变量名
则会展开变量值;在遇到``则会命令替换。
bash启动脚本
就是bash启动时执行的脚本,这里边的规则还是挺复杂的,但是只需要记住一条,我们可以将环境变量、alias、mask定义到.bashrc文件中即可。这样在bash启动时会自动source启动脚本,这样我们预先定义的变量就自动生效了。
shell脚本语法
条件测试
条件测试语句在shell中有两种表达[]
/[[]]
/test
,条件测试语句可以测试字符串/数值/文件的属性。比如
VAR=2
test $VAR -gt 1$
[ $VAR -gt 3 ]
不管用那种方式,最终表示条件是真还是假,是通过$?
来判断的,0表示true,1表示false。
对于与或非
在shell中[[]]
是[]
的拓展,并且[[]]
是兼容[]
的。竟然是拓展,那功能肯定要比之前的[]
要强一些的。
- 替换掉
[]
中的-a
或者-o
[ -f README.md -a -x README.md ] && echo "yes" || echo "no"
# 如果使用[[]]的话,可以这样:
[[ -f README.md && -x README.md ]] && echo "yes" || echo "no"
- 使用正则表达式
A="hello"; [[ "$A" =~ hell? ]] && echo "yes" || echo "no"
# 这里要注意=~ 右边不能使用""。如果使用""就表示字符串了,不使用""就表示正则的模式
判断语句
在shell编程中是可以使用if
语句的,但是跟我们的c语言的if也不太一样。
if [ -f ~/.bashrc ]; then
. ~/.bashrc
fi
其实涉及三条语句,if [ -f ~/.bashrc ]
如果测试语句$?为0则表示为true,否则为false。then . ~/.bashrc
在shell一般一行只有一条语句,如果要有多条语句的话,就用;分离。fi
表示if的结束标记。
特殊if语句,也就是if的条件永远为true。:
是空语句,执行结果$?是0。
if :; then echo "always true"; fi
常见的if
用法为:
num=1
if [ $(($num)) -gt 10 ]; then
echo 'a'
elif [ ${num} -eq 10 ]; then
echo 'b'
else
echo 'c'
fi
&&
、||
用法,其实就是利用短路特性。
test "$(whoami)" != 'root' && (echo you are using a non-privileged account; exit 1)
对于if
语句最佳实践,最好对于if [ $var xxxx ]
中的$var
用""
括起来。
case语句
case语句就是c语言中的switch case。大概的用法是这样:
case $1 in
start)
...
;;
stop)
...
;;
reload | force-reload)
...
;;
restart)
...
;;
*)
log_success_msg "Usage: /etc/init.d/apache2 {start|stop|restart|reload|force-reload|start-htcacheclean|stop-htcacheclean}"
exit 1
;;
esac
以case开头,在in里边进行匹配,支持通配符,一旦找到某一个条件就执行对应的语句最终语句是以;;
结束。整个语句是以esac结束。
for/do/done
for循环。比如
for FRUIT in apple banana pear; do
echo "I like $FRUIT"
done
如果想将某一个目录下的文件改名,可以这样:
files=`ls test`
echo ${files}
for f in ${files}; do
mv "test/${f}" "test/${f}.tmp"
done
也可以写类似于c语言的循环形式
declare -i mi
declare -i mx=100
declare -i s=0
for((mi=0; mi <= mx; mi=mi+1)); do
s=s+mi
done
echo $s
while/do/done
COUNTER=1
while [ "$COUNTER" -lt 10 ]; do
echo "Here we go again"
COUNTER=$(($COUNTER+1))
done
这个应该很好理解。
位置参数与特殊变量
可以参考如下列表:
其中位置参数可以用shift命令左移。比如shift 3表示原来的1,原来的2等等,原来的2、0不移动。不带参数的shift命令相当于shift 1。shift可以使用场景就是在脚本内部可能会调用其他的脚本,但是参数又不想用那么多,这个时候就可以使用shift进行丢弃。
函数
shell中的函数不用写参数列表跟返回值类型的。是可以传递参数跟返回值的,只不过不需要声明而已。
is_directory()
{
DIR_NAME=$1
if [ ! -d $DIR_NAME ]; then
return 1
else
return 0
fi
}
for DIR in "$@"; do
if is_directory "$DIR"
then :
else
echo "$DIR doesn't exist. Creating it now..."
mkdir $DIR > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "Cannot create directory $DIR"
exit 1
fi
fi
done
在shell脚本中如果要表示返回true,一般用return 0,这个跟if
判断是有关系的。另外在shell空语句用:
来表达。
常见技巧积累
如果脚本遇到错误,最好能暴露出来,不要埋雷
这种情况下,我们一般要不使用set -e
或者是指定脚本解释器时,指定-e
参数,比如这样:
#!/bin/bash -e
#set -e
mkdir -p a/b
cd a
touch file
cd -
rmdir a
echo "done"
这样脚本一旦遇到error就会停止运行,并且$?
也会返回非0
显性指定脚本的工作目录
也就是说,不管使用者从那个位置启动脚本,该脚本都能一如既往的按照预期的目录工作。一般情况下我们将某个脚本放置在某个目录,那一般工作路径基本上是基于该目录的,所以我们最好再写脚本时,在脚本的最开头这样写:
dir=$(dirname $(readlink -f "$0"))
cd ${dir}
这种写法也同时考虑了软连接的情况
脚本的命令行参数处理技巧
最简单的处理,应该是一个for再加一个case语句来处理,复杂点的参数可以通过getopt
来实现。
#!/bin/bash
for arg in $@; do
case $arg in
-h)
echo "usage:xxxx"
exit 0
;;
-a)
echo "param a handle"
;;
*)
echo "default param handle"
;;
esac
done
其他
|
命令的右边必须是能接受标准输入作为参数时才能正常运行。比如echo "helloworld" | echo
就不会有任何输出,可以通过xargs
命令将标准输入转成命令行参数, echo "helloworld | xargs echo
就可以正常打印了