shell相关2

变量

显示所有环境变量:

$ env
# 或者
$ printenv

显示某一个环境变量名:

$ echo $HOME
/root
# 或者
$ printenv HOME
/root

Bash 变量名区分大小写,HOME和home是两个不同的变量。
自定义变量是用户在当前 Shell 里面自己定义的变量,仅在当前 Shell 可用。一旦退出当前 Shell,该变量就不存在了。

显示所有变量(包括环境变量和自定义变量),以及所有的 Bash 函数:

$ set

一些变量赋值:

a=z                     # 变量 a 赋值为字符串 z
b="a string"            # 变量值包含空格,就必须放在引号里面
c="a string and $b"     # 变量值可以引用其他变量的值
#echo c   -->   "a string and a string"

由于$在 Bash 中有特殊含义,把它当作美元符号使用时,一定要非常小心:

$ echo The total is $100.00
The total is 00.00
$ echo The total is \$100.00
The total is $100.00

读取变量的时候,变量名也可以使用花括号{}包围,比如$a也可以写成${a}。这种写法可以用于变量名与其他字符连用的情况:

$ a=foo
$ echo $a_file

$ echo ${a}_file
foo_file

如果变量的值本身也是变量,可以使用${!varname}的语法,读取最终的值:

$ myvar=USER
$ echo ${myvar}
USER
$ echo ${!myvar}
ruanyf

如果变量值包含连续空格(或制表符和换行符),最好放在双引号里面读取:

$ a="1 2  3"
$ echo $a
1 2 3
$ echo "$a"
1 2  3

unset命令用来删除一个变量:

unset NAME

不存在的 Bash 变量一律等于空字符串,所以即使unset命令删除了变量,还是可以读取这个变量,值为空字符串。

用户创建的变量仅可用于当前 Shell,子 Shell 默认读取不到父 Shell 定义的变量。为了把变量传递给子 Shell,需要使用export命令。这样输出的变量,对于子 Shell 来说就是环境变量。
export命令用来向子 Shell 输出变量:

NAME=foo
export NAME
#或者
export NAME=value

举例:

# 输出变量 $foo
$ export foo=bar

# 新建子 Shell
$ bash

# 读取 $foo
$ echo $foo
bar

# 修改继承的变量
$ foo=baz

# 退出子 Shell
$ exit

# 读取 $foo,子shell不影响父shell
$ echo $foo
bar

$?为上一个命令的退出码,用来判断上一个命令是否执行成功。返回值是0,表示上一个命令执行成功;如果不是零,表示上一个命令执行失败:

$ ls doesnotexist
ls: doesnotexist: No such file or directory

$ echo $?
1

$$为当前 Shell 的进程 ID:

$ echo $$
10662

$_为上一个命令的最后一个参数:

$ grep dictionary /usr/share/dict/words
dictionary

$ echo $_
/usr/share/dict/words

$!为最近一个后台执行的异步命令的进程 ID:

$ firefox &
[1] 11064

$ echo $!
11064

上面例子中,firefox是后台运行的命令,$!返回该命令的进程 ID。

$0为当前 Shell 的名称(在命令行直接执行时)或者脚本名(在脚本中执行时):

$ echo $0
bash

$-为当前 Shell 的启动参数:

$ echo $-
himBHs

$#表示脚本的参数数量,$@表示脚本的参数值。

如下表示如果变量varname存在且不为空,则返回它的值,否则返回word。比如${count:-0}表示变量count不存在时返回0:

${varname:-word}

如果变量varname存在且不为空,则返回它的值,否则将它设为word,并且返回word:

${varname:=word}

如果变量名存在且不为空,则返回word,否则返回空值。它的目的是测试变量是否存在:

${varname:+word}

如果变量varname存在且不为空,则返回它的值,否则打印出varname: message,并中断脚本的执行。它的目的是防止变量未定义,比如${count:?"undefined!"}表示变量count未定义时就中断执行,抛出错误,返回给定的报错信息undefined!:

${varname:?message}

上面四种语法如果用在脚本中,变量名的部分可以用数字1到9,表示脚本的参数:

#1表示脚本的第一个参数。如果该参数不存在,就退出脚本并报错。
filename=${1:?"filename missing."}

declare命令可以声明一些特殊类型的变量,为变量设置一些限制,比如声明只读类型的变量和整数类型的变量:

declare OPTION VARIABLE=value

-i参数声明整数变量以后,可以直接进行数学运算:

$ val1=12 val2=5
$ declare -i result
$ result=val1*val2
$ echo $result
60

-x参数等同于export命令,可以输出一个变量为子 Shell 的环境变量:

$ declare -x foo
# 等同于
$ export foo

-r参数可以声明只读变量,无法改变变量值,也不能unset变量:

$ declare -r bar=1

$ bar=2
bash: bar:只读变量
$ echo $?
1

$ unset bar
bash: bar:只读变量
$ echo $?
1

readonly命令等同于declare -r,用来声明只读变量,不能改变变量值,也不能unset变量:

$ readonly foo=1
$ foo=2
bash: foo:只读变量
$ echo $?
1

let命令声明变量时,可以直接执行算术表达式:

$ let foo=1+2
$ echo $foo
3

let命令的参数表达式如果包含空格,就需要使用引号:

$ let "foo = 1 + 2"

let可以同时对多个变量赋值,赋值表达式之间使用空格分隔:

$ let "v1 = 1" "v2 = v1++"    #先赋值,再自增
$ echo $v1,$v2
2,1

字符串操作

获取字符串长度:

${#varname}

比如:

$ myPath=/home/cam/book/long.file.name
$ echo ${#myPath}
29

字符串提取子串的语法如下:

${varname:offset:length}

比如:

$ count=frogfootman
$ echo ${count:4:4}
foot

需要通过变量名传入,不可直接传入字符串:

# 报错
$ echo ${"hello":2:3}

参数为负数:

$ foo="This string is long."
$ echo ${foo: -5}   #注意-5前面有空格,与${variable:-word}区分
long.
$ echo ${foo: -5:2}
lo
$ echo ${foo: -5:-2}   #如果length为-2,表示要排除从字符串末尾开始的2个字符,所以返回lon
lon

字符串头部匹配:

# 如果 pattern 匹配变量 variable 的开头,
# 删除最短匹配(非贪婪匹配)的部分,返回剩余部分
${variable#pattern}

# 如果 pattern 匹配变量 variable 的开头,
# 删除最长匹配(贪婪匹配)的部分,返回剩余部分
${variable##pattern}

比如:

$ myPath=/home/cam/book/long.file.name

$ echo ${myPath#/*/}
cam/book/long.file.name

$ echo ${myPath##/*/}
long.file.name

#如果匹配不成功,则返回原始字符串:
$ echo ${myPath#aaa}
/home/cam/book/long.file.name

如果要将头部匹配的部分,替换成其他内容,采用下面的写法:

# 模式必须出现在字符串的开头
${variable/#pattern/string}

# 示例
$ foo=JPG.JPG
$ echo ${foo/#JPG/jpg}
jpg.JPG

同样的,字符串尾部匹配:

# 如果 pattern 匹配变量 variable 的结尾,
# 删除最短匹配(非贪婪匹配)的部分,返回剩余部分
${variable%pattern}

# 如果 pattern 匹配变量 variable 的结尾,
# 删除最长匹配(贪婪匹配)的部分,返回剩余部分
${variable%%pattern}

如果要将尾部匹配的部分,替换成其他内容,采用下面的写法:

# 模式必须出现在字符串的结尾
${variable/%pattern/string}

# 示例
$ foo=JPG.JPG
$ echo ${foo/%JPG/jpg}
JPG.jpg

任意位置匹配:

# 如果 pattern 匹配变量 variable 的一部分,
# 最长匹配(贪婪匹配)的那部分被 string 替换,但仅替换第一个匹配
${variable/pattern/string}

# 如果 pattern 匹配变量 variable 的一部分,
# 最长匹配(贪婪匹配)的那部分被 string 替换,所有匹配都替换
${variable//pattern/string}

比如:

$ path=/home/cam/foo/foo.name

$ echo ${path/foo/bar}
/home/cam/bar/foo.name

$ echo ${path//foo/bar}
/home/cam/bar/bar.name

配合通配符:

$ phone="555-456-1414"
$ echo ${phone/5?4/-}
55-56-1414

下面的语法可以改变变量的大小写:

# 转为大写
${varname^^}

# 转为小写
${varname,,}

比如:

$ foo=heLLo
$ echo ${foo^^}
HELLO
$ echo ${foo,,}
hello

((...))语法可以进行整数的算术运算:

$ ((foo = 5 + 5))
$ echo $foo
10

这个语法不返回值,命令执行的结果根据算术运算的结果而定。只要算术结果不是0,命令就算执行成功:

$ (( 3 + 2 ))
$ echo $?
0
$ (( 3 - 3 ))
$ echo $?
1

如果要读取算术运算的结果,需要在((...))前面加上美元符号$((...)),使其变成算术表达式,返回算术运算的值:

$ echo $((2 + 2))
4

这个语法只能计算整数,否则会报错:

# 报错
$ echo $((1.5 + 1))
bash: 语法错误

$((...))的圆括号之中,不需要在变量名之前加上$,不过加上也不报错:

$ number=2
$ echo $(($number + 1))
3

如果在$((...))里面使用字符串,Bash 会认为那是一个变量名。如果不存在同名变量,Bash 就会将其作为空值,因此不会报错:

$ echo $(( "hello" + 2))
2
$ echo $(( "hello" * 2))
0
$ hello=1
$ echo $(("hello"+1))
2

可以进行动态替换:

$ foo=hello
$ hello=3
$ echo $(( foo + 2 ))
5

$[...]是以前的语法,也可以做整数运算,不建议使用:

$ echo $[2+2]
4

bash中的进制:

number:没有任何特殊表示法的数字是十进制数(以10为底)。
0number:八进制数。
0xnumber:十六进制数。
base#number:base进制的数。

比如:

$ echo $((0xff))
255
$ echo $((2#11111111))
255

位运算:

<<:位左移运算,把一个数字的所有位向左移动指定的位。
>>:位右移运算,把一个数字的所有位向右移动指定的位。
&:位的“与”运算,对两个数字的所有位执行一个AND操作。
|:位的“或”运算,对两个数字的所有位执行一个OR操作。
~:位的“否”运算,对一个数字的所有位取反。
^:位的异或运算(exclusive or),对两个数字的所有位执行一个异或操作。

比如:

$ echo $((16>>2))
4

逻辑运算:
$((...))支持以下逻辑运算:

<:小于
>:大于
<=:小于或相等
>=:大于或相等
==:相等
!=:不相等
&&:逻辑与
||:逻辑或
!:逻辑否
expr1?expr2:expr3:三元条件运算符。若表达式expr1的计算结果为非零值(算术真),则执行表达式expr2,否则执行表达式expr3。

比如:

$ echo $(( (3 > 2) || (4 <= 1) ))
1

$ a=0
$ echo $((a<1 ? 1 : 0))
1
$ echo $((a>1 ? 1 : 0))
0

((...))可以执行赋值运算:

$ echo $((a=1))
1
$ echo $a
1

如果在表达式内部赋值,可以放在圆括号中,否则会报错:

$ a=0
$ echo $(( a<1 ? (a+=1) : (a-=1) ))
1

逗号,$((...))内部是求值运算符,执行前后两个表达式,并返回后一个表达式的值:

$ echo $((foo = 1 + 2, 3 * 4))   #两个表达式都会执行,然后返回后一个表达式的值12
12
$ echo $foo
3

expr命令支持算术运算,可以不使用((...))语法,也只支持整数运算:

$ expr 3 + 2
5

$ foo=3
$ expr $foo + 2
5

let命令用于将算术运算的结果,赋予一个变量:

$ let x=2+3
$ echo $x
5

注意,x=2+3这个式子里面不能有空格,否则会报错。

bash会保留历史操作在文件中,环境变量HISTFILE总是指向这个文件。

$ echo $HISTFILE
/home/me/.bash_history

history命令会输出这个文件的全部内容。用户可以看到最近执行过的所有命令,每条命令之前都有行号。越近的命令,排在越后面:

$ history
...
498 echo Goodbye
499 ls ~
500 cd

输入命令时,按下Ctrl + r快捷键,就可以搜索操作历史,选择以前执行过的命令。这时键入命令的开头部分,Shell 就会自动在历史文件中,查询并显示最近一条匹配的结果,这时按下回车键,就会执行那条命令。

!e表示找出操作历史之中,最近的那一条以e开头的命令并执行。Bash 会先输出那一条命令echo Goodbye,然后直接执行:

$ echo Hello World
Hello World

$ echo Goodbye
Goodbye

$ !e
echo Goodbye
Goodbye

由于!string语法会扩展成以前执行过的命令,所以含有!的字符串放在双引号里面,必须非常小心:

$ echo "hello!"     #双引号中!后无字符
hello!

$ echo "I say:\"hello!\""    #双引号中!后有字符
bash: !\: event not found

#需要对!转义:
$ echo "I say:\"hello\!\""
I say:"hello\!"

上面的命令会报错,原因是感叹号后面是一个反斜杠,Bash 会尝试寻找,以前是否执行过反斜杠开头的命令,一旦找不到就会报错。解决方法就是在感叹号前面,也加上反斜杠。

在history中显示时间戳:

$ export HISTTIMEFORMAT='%F %T  '
$ history
1  2013-06-09 10:40:12   cat /etc/issue
2  2013-06-09 10:40:12   clear

上面代码中,%F相当于%Y - %m - %d%T相当于%H : %M : %S

环境变量HISTSIZE设置保存历史操作的数量:

$ export HISTSIZE=10000

上面命令设置保存过去10000条操作历史。

如果不希望保存本次操作的历史,可以设置HISTSIZE等于0:

export HISTSIZE=0

如果HISTSIZE=0写入用户主目录的~/.bashrc文件,那么就不会保留该用户的操作历史。如果写入/etc/profile,整个系统都不会保留操作历史。

环境变量HISTIGNORE可以设置哪些命令不写入操作历史:

export HISTIGNORE='pwd:ls:exit'

上面示例设置,pwdlsexit这三个命令不写入操作历史。

操作历史的每一条记录都有编号。知道了命令的编号以后,可以用感叹号 + 编号执行该命令。如果想要执行.bash_history里面的第8条命令,可以像下面这样操作:

$ !8

history命令的-c参数可以清除操作历史:

$ history -c

cd -命令可以返回前一次的目录:

# 当前目录是 /path/to/foo
$ cd bar

# 重新回到 /path/to/foo
$ cd -

如果希望记忆多重目录,可以使用pushd命令和popd命令。它们用来操作目录堆栈:
pushd命令的用法类似cd命令,可以进入指定的目录,并将该目录放入堆栈:

$ pushd dirname

第一次使用pushd命令时,会将当前目录先放入堆栈,然后将所要进入的目录也放入堆栈,位置在前一个记录的上方。以后每次使用pushd命令,都会将所要进入的目录,放在堆栈的顶部。
popd命令不带有参数时,会移除堆栈的顶部记录,并进入新的栈顶目录(即原来的第二条目录)。
比如:

# 当前处在主目录,堆栈为空
$ pwd
/home/me

# 进入 /home/me/foo
# 当前堆栈为 /home/me/foo /home/me
$ pushd ~/foo

# 进入 /etc
# 当前堆栈为 /etc /home/me/foo /home/me
$ pushd /etc

# 进入 /home/me/foo
# 当前堆栈为 /home/me/foo /home/me
$ popd

# 进入 /home/me
# 当前堆栈为 /home/me
$ popd

# 目录不变,当前堆栈为空
$ popd

这两个命令的参数如下:
-n的参数表示仅删除堆栈顶部的记录,不改变目录,执行完成后还停留在当前目录:

$ popd -n

这两个命令还可以接受一个整数作为参数,该整数表示堆栈中指定位置的记录(从0开始)。pushd命令会把这条记录移动到栈顶,同时切换到该目录;popd则从堆栈中删除这条记录,不会切换目录:

# 将从栈顶算起的3号目录(从0开始)移动到栈顶,同时切换到该目录
$ pushd +3

# 将从栈底算起的3号目录(从0开始)移动到栈顶,同时切换到该目录
$ pushd -3

# 删除从栈顶算起的3号目录(从0开始),不改变当前目录
$ popd +3

# 删除从栈底算起的3号目录(从0开始),不改变当前目录
$ popd -3

dirs命令可以显示目录堆栈的内容,一般用来查看pushd和popd操作后的结果:

$ dirs
~/foo/bar ~/foo ~

栈顶(最晚入栈的目录)在最左边,栈底(最早入栈的目录)在最右边。
dirs -c:清空目录栈。

脚本一般以Shebang行开头:

#!/bin/sh
# 或者
#!/bin/bash

如果bash解释器不在/bin目录,也可以这么写:

#!/usr/bin/env bash

解释:env命令会返回环境变量。当执行 env python 时,它其实会去 env | grep PATH 里(也就是 /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin)这几个路径里去依次查找名为 python 的可执行文件。

脚本入门

如果将脚本放在环境变量$PATH指定的目录中,就不需要指定路径了。因为 Bash 会自动到这些目录中,寻找是否存在同名的可执行文件。
建议在主目录新建一个~/bin子目录,专门存放可执行脚本,然后把~/bin加入$PATH:

export PATH=$PATH:~/bin

可以将这一行加到~/.bashrc文件里面,然后重新加载一次.bashrc,这个配置就可以生效了:

$ source ~/.bashrc

以后不管在什么目录,直接输入脚本文件名,脚本就会执行:

$ script.sh

Bash 脚本中,#表示注释。

调用脚本可以带参数:

$ script.sh word1 word2 word3

脚本文件内部,可以使用特殊变量,引用这些参数:

$0:脚本文件名,即script.sh。
$1~$9:对应脚本的第一个参数到第九个参数。
$#:参数的总数。
$@:全部的参数,参数之间使用空格分隔。
$*:全部的参数,参数之间使用变量$IFS值的第一个字符分隔,默认为空格,但是可以自定义。

如果脚本的参数多于9个,那么第10个参数可以用${10}的形式引用,以此类推。

比如script.sh如下:

#!/bin/bash
# script.sh

echo "全部参数:" $@
echo "命令行参数数量:" $#
echo '$0 = ' $0
echo '$1 = ' $1
echo '$2 = ' $2
echo '$3 = ' $3

运行结果如下:

$ ./script.sh a b c
全部参数:a b c
命令行参数数量:3
$0 =  script.sh
$1 =  a
$2 =  b
$3 =  c

如果多个参数放在双引号里面,视为一个参数。

$ ./script.sh "a b"

上面例子中,Bash 会认为"a b"是一个参数,$1会返回a b。注意,返回时不包括双引号。

shift命令:
shift命令可以改变脚本参数,每次执行都会移除脚本当前的第一个参数($1),使得后面的参数向前一位,即$2变成$1$3变成$2$4变成$3,以此类推:

#!/bin/bash
#while循环结合shift命令,也可以读取每一个参数
echo "一共输入了 $# 个参数"

while [ "$1" != "" ]; do
  echo "剩下 $# 个参数"
  echo "参数:$1"
  shift
done

shift命令的默认参数为1,如上。也可以shift 3,表示移除前三个参数,原来的$4变成$1

getopts命令
getopts命令用在脚本内部,用于取出脚本所有的带有前置连词线-的参数(配置项参数):

getopts optstring name

第一个参数optstring是字符串,给出脚本所有的连词线参数。比如,某个脚本可以有三个配置项参数-l-h-a,其中只有-a可以带有参数值,而-l-h是开关参数,那么getopts的第一个参数写成lha:,顺序不重要。注意,a后面有一个冒号,表示该参数带有参数值。getopts的第二个参数name是一个变量名,用来保存当前取到的配置项参数,即l、h或a。
比如使用getopts命令配合while循环处理参数:

while getopts 'lha:' OPTION; do
  case "$OPTION" in
    l)
      echo "linuxconfig"
      ;;

    h)
      echo "h stands for h"
      ;;

    a)
      avalue="$OPTARG"   #如果某个连词线参数带有参数值,比如-a foo,那么处理a参数的时候,环境变量$OPTARG保存的就是参数值。
      echo "The value provided is $OPTARG"
      ;;
    ?)        #如果用户输入了没有指定的参数(比如-x),那么OPTION等于?。
      echo "script usage: $(basename $0) [-l] [-h] [-a somevalue]" >&2
      exit 1
      ;;
  esac
done
shift "$(($OPTIND - 1))"   #变量$OPTIND在getopts开始执行前是1,然后每次执行就会加1。

basename命令用于去掉文件名的目录和后缀。
变量$OPTIND在getopts开始执行前是1,然后每次执行就会加1。等到退出while循环,就意味着连词线参数全部处理完毕。这时,$OPTIND - 1就是已经处理的连词线参数个数,使用shift命令将这些参数移除,保证后面的代码可以用$1$2等处理命令的主参数。

---开头的参数,会被 Bash 当作配置项解释。那如果需要创建文件名为--test,需使用配置项参数终止符--,表示后面的参数都是实体参数:

$ touch -- --test

比如:

$ myPath="-l"
$ ls -- $myPath
ls: 无法访问'-l': 没有那个文件或目录

如果想在文件里面搜索--hello,这时也要使用参数终止符--

$ grep -- "--hello" example.txt

如果不用参数终止符,grep命令就会把--hello当作配置项参数,从而报错。

exit命令用于终止当前脚本的执行,并向 Shell 返回一个退出值。exit命令后面可以跟参数,该参数就是退出状态:

# 退出值为0(成功)
$ exit 0

# 退出值为1(失败)
$ exit 1

比如:

if [ $(id -u) != "0" ]; then
  echo "根用户才能执行当前脚本"
  exit 1
fi

id -u命令返回用户的 ID,一旦用户的 ID 不等于0(根用户的 ID),脚本就会退出,并且退出码为1,表示运行失败。

命令执行结束后,会有一个返回值。0表示执行成功,非0(通常是1)表示执行失败。环境变量$?可以读取前一个命令的返回值:

cd /path/to/somewhere
if [ "$?" = "0" ]; then
  rm *
else
  echo "无法切换目录!" 1>&2
  exit 1
fi

也可以使用if直接判断命令执行结果:

if cd /path/to/somewhere; then
  rm *
else
  echo "Could not change directory! Aborting." 1>&2
  exit 1
fi

正常的执行脚本文件,是类似于bash *.sh,其实是调用子shell。而source命令不会调用子shell,所以常用于加载配置文件:
比如:

#!/bin/bash
# test.sh
echo $foo

使用不同方式调用:

# 当前 Shell 新建一个变量 foo
$ foo=1

# 打印输出 1
$ source test.sh
1

# 打印输出空字符串   #这一步如果想要输出1则需要在之前export foo=1
$ bash test.sh

source命令的另一个用途,是在脚本内部加载外部库:

#!/bin/bash

source ./lib.sh

function_from_lib

source有一个简写形式,可以使用一个点.来表示:

$ . .bashrc

alias命令用来为一个命令指定别名,这样更便于记忆:

alias NAME=DEFINITION

比如:

alias search=grep

alias也可以用来为长命令指定一个更短的别名。下面是通过别名定义一个today的命令:

$ alias today='date +"%A, %B %-d, %Y"'
$ today
星期一, 一月 6, 2020

alias定义的别名也可以接受参数,参数会直接传入原始命令(其实就是个替换的过程):

$ alias echo='echo It says: '
$ echo hello world
It says: hello world

直接调用alias命令,可以显示所有别名:

$ alias

unalias命令可以解除别名:

$ unalias lt

参考:https://wangdoc.com/bash/script.html

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容