Posts shell 学习笔记
Post
Cancel

shell 学习笔记

基础

括号的使用总结

单括号、双括号、单方括号、双方括号

  • 单括号 () 没作用;
  • $() 是命令替换;
  • (()) 可以进行算数运算,c 语言风格的 for 命令;
  • 单方括号 [] 是测试命令,需要有空格
  • $[] 是数学运算,作用与 $(()) 一样,后者更常用;
  • $[[]] 是高级的字符串运算,需要有空格

如果两个命令一起运行,可以把它们放在同一行,之间使用分号隔开:

1
$ date ; who

使用 ./test1 的方式执行脚本,需要文件有执行权限,用 chmod u+x 赋予权限。另外,创建文件时,umask 的值决定了新文件的默认权限设置。(参加书本第 7 章)

使用 sh test1 的方式执行脚本,不需要文件有执行权限。

使用 echo 需要注意的点:

  • 默认情况下,不需要使用引号,但是字符串里面有单引号或双引号就有问题了
  • 可用单引号或双引号
  • 使用 -n 参数可以不换行,不知为何使用 sh 执行时,-n 不起作用?
  • 单引号中不能使用变量

因此建议,养成使用双引号的习惯。

使用环境变量:

  • set 命令查看当前环境变量列表;
  • 使用 echo $PATHecho $USER 等方法使用;

使用变量

  • 在变量、等号和值之间不能出现空格;
  • 区分大小写;
  • 引用变量有两种方式:$variable${variable}

命令替换

  • 反引号 `
  • $() 格式,最好用这种方式。
1
2
testing=`date`
testing=$(date +%y%m%d)

内联输入重定向:

使用符号 <<,还必须指定一个文本标记来划分输入数据的开始和结尾。任何字符串都可作为文本标记,但在数据的开始和结尾文本标记必须一致。shell 会用 PS2 环境变量(参见第六章)中定义的次提示符来提示输入数据。

1
2
3
4
5
6
$ wc << EOF
heredoc> test string 1
heredoc> test string 2
heredoc> test string 3
heredoc> EOF
       3       9      42

管道:

  • 管道两边的命令会同时运行
  • 第一个命令输出的同时,输出会被立即送给第二个命令。数据传输不会用到任何中间文件或缓冲区。
1
$ rpm -qa | sort

数学运算

bash shell 只支持整数运算。

三种方式:

  • expr 命令,符号两边必须有空格,特殊符号如 * 需要转义
  • 使用方括号$[operation],不必注意空格,可以使用任何算数运算
  • 使用双括号 $((operation))推荐使用。其实相当于双括号里面执行算数运算。
1
2
3
4
$(expr 5 \* 2)
$(expr 5 + 2)
var4=$[$var1 * ($var2 - $var3)]
$((var2 * var1))

浮点数运算,使用内建的 bash 计算器——bc

使用 bc 命令进入计算器,quit 退出。-q 命令行选项可以不显示欢迎信息。

浮点数运算是由 scale 变量控制的,如果不指定的话,就是 0

bash计算器还能支持变量

1
2
3
4
5
6
7
8
9
10
11
12
13
$ bc -q
3.14/5
0
scale=4
3.14/5
.6280

var1=10
var2=10*2
print var2
20
quit
$ 

在脚本中使用 bc,使用命令替换的方式,格式为:

variable=$(echo "options; expression" | bc)

1
2
3
4
5
#!/bin/bash
var1=100
var2=45
var3=$(echo "scale=4; $var1 / $var2" | bc)
echo "The answer for this is $var3"

bc 和内联重定向结合使用,可以让多个运算过程变得清晰,需要注意的是,在计算器里面创建的变量,不能在 shell 脚本中使用。

1
2
3
4
5
6
7
8
# bc 和内联输入重定向结合使用
var8=$(bc << EOF
scale = 4
a1 = $var4 * $var5
b1 = $var6 * $var7
a1 + b1
EOF
)

退出脚本

退出状态码是一个 0~255的整数值,一个成功结束的命令的退出状态码是 0。如果超过这个范围,最终的结果就是指定的数值除以 256 后得到的余数。

使用 $? 来保存上个已执行命令的退出状态码。

使用 exit 命令指定退出状态码。

常用的退出状态码:

状态码描述
0命令成功结束
1一般性未知错误
2不适合的shell命令
126命令不可执行
127没找到命令
128无效的退出参数
128+x与 Linux 信号 x 相关的严重错误
130通过 Ctrl+C 终止的命令
255正常范围之外的退出状态码

数组

对其它 shell 而言,数组变量的可移植性并不好,应尽量减少使用。下面的例子在 zsh 中运行,结果就不一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#!/bin/bash

# 定义数组
myarray=(one two three four)

# one
echo $myarray

# one
echo ${myarray[0]}

# one two three four
echo ${myarray[*]}

myarray[2]=seven

unset myarray[2]

# one two four
echo ${myarray[*]}

# 这个位置是空的
echo ${myarray[2]}

# four
echo ${myarray[3]}

unset myarray

# 空的
echo ${myarray[*]}

循环语句

if-then 语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if command
then
	commands
fi

# 或者

if command; then
	commands
fi

if grep $testuser /et/passwd
then
	...
fi

if-then-else

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if command
then
	commands
else
	commands
fi

if command
then
	commands
elif command
then
	commands
fi

test 命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# test 命令没有命令的话,以非零的退出状态码退出
if test
then
    echo "test command is ok."
else
    echo "test command is false."
fi

# 可以使用 test 命令确定变量中是否有内容
my_variable="23"
if test $my_variable
then
    echo "variable is not empty."
else
    echo "variable is empty."
fi

使用方括号定义测试条件,第一个方括号之后和第二个方括号之前必须有空格

  • 数值比较
  • 字符串比较
  • 文件比较

数值比较

n1 -eq n2

n1 -ge n2

n1 -gt n2

n1 -le n2

n1 -lt n2

n1 -ne n2

1
2
3
4
5
6
7
8
9
10
11
12
# 方括号前后必须要有空格
# test 命令中不能使用浮点数
if [ $value1 -gt $value2 ]
then
    echo "$value1 is greater than $value2"
elif [ $value1 -lt $value2 ]
then
    echo "$value1 is less than $value2"
else
    echo "$value1 is equal $value2"
fi
echo

字符串比较

str1 = str2

str1 != str2

str1 < str2

str1 > str2

-n str1 检查 str1 的长度是否非0

-z str1 检查 str1 的长度是否为0

注意,字符串顺序比较中:

  • 大于号和小于号必须转义
  • 大于和小于顺序和 sort 命令所采用的不同

在比较测试中,大写字母被认为是小于小写字母的。而 sort 命令相反。

比较测试中用的是标准的 ASCII 顺序。sort 命令使用的是系统的本地化语言设置中定义的排序顺序。对于英语,本地化设置指定了在排序顺序中小写字母出现在大写字母前。

1
2
3
4
5
6
7
8
9
if [ $USER != $testuser ]
then
	...
fi

if [ $var1 \> $var2 ]
then
	...
fi

文件比较

-d file

-f file

-e file

-r file

-w file

-x file

-O file 检查 file 是否存在并属于当前用户所有

-G file 检查 file 是否存在并且默认组与当前用户相同

-s file 检查 file 是否存在并非空

file1 -nt file2

file1 -ot file2

使用 -nt-ot 比较文件之前,最后先检查文件是否存在:

1
2
3
4
5
6
7
8
9
10
# 文件不存在,返回 false
if [ badfile1 -nt badfile2 ]
then
	echo "The badfile1 file is newer than badfile2"
else
	echo "The badfile1 file is older than badfile2"
fi

# 结果是
The badfile1 file is older than badfile2

复合条件测试

1
2
3
[ condition1 ] || [ condition2 ]

[ condition1 ] && [ condition2 ]

双括号和双方括号

双括号里面可以进行算数运算。

双方括号里面可以进行字符串处理。

记住,不管是方括号还是双方括号,都需要有空格。而单括号的命令替换和双括号的算数运算,不需要有空格。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 双括号里面不需要使用 $ 
if ((val1 ** 2 > 90))
then
    # 赋值时 = 两边可以有空格
    ((val2 = val1 ** 2))
    echo "The square of $val1 is $val2"
fi

# 方括号两边必需要有空格,模式匹配功能
if [[ $USER == x* ]]
then
    echo "Hello $USER."
else
    echo "Sorry, I do not know you."
fi

case 命令

1
2
3
4
5
case variable in
pattern1 | pattern2) commands1;;
pattern3) commands2;;
*) default commands;;
esac

注意结尾的双分号

1
2
3
4
5
6
7
8
9
10
11
case $USER in
xiaocan | barbara)
    echo "Welcome, $USER"
    echo "Please enjoy your visit.";;
testing)
    echo "Special testing account.";;
jessica)
    echo "Do not forget to log off when you're done.";;
*)
    echo "Sorry, your are not allowed here";;
esac

for 命令

1
2
3
4
for var in list
do
	command
done

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
for test in Alabama Alaska Arizona Arkasas California
do
    echo The next state is $test
done

# 不同于其他编程语言,在 for 循环结束后,也能够使用 test 变量
echo $test

# 对单引号转义,或者使用双引号包括它
for test in I don\'t know if "this'll" work
do
    echo "word: $test"
done

# 使用双引号包括多个词汇
for test in Nevada "New Hampshire" "New York"
do
    echo "City: $test"
done

# 字符串拼接的常用方法
list=$list" Connecticut"


# 怎么打印带有空格的字符呢?
files="states"
# 更新字段的分割符
IFS=$'\n'
for state in $(cat $files)
do
    echo "Visit beautiful $state"
done

for 循环假定每个值都是空格分割的。使用 IFS (内部字段分隔符 internal field separator)指定分隔符。

1
2
3
4
5
6
7
8
IFS.OLD=$IFS
IFS=$'\n'
# 之后再还原 IFS 的值
IFS=$IFS.OLD

IFS=:
# 换行符、冒号、分号、双引号
IFS=$'\n':;"

读取目录:

1
2
3
4
5
6
7
8
9
10
11
12
# 可以指定多个目录
for file in /Users/xiaocan/Documents/learn/shell-learn/* /home/rich/badtest
do
    # 防止 file 名称有空格,所以添加引号
    if [ -d "$file" ]
    then
        echo "$file is a directory."
    elif [ -f "$file" ]
    then
        echo "$file is a file."
    fi
done

C 语言风格的 for 命令

1
2
3
4
5
6
7
8
9
10
11
# 括号两边的空格不是必需的
for (( i = 1; i <= 10; i++ ))
do
    echo "The next number is $i"
done
echo

for (( a=1, b=10; a <= 10; a++, b--))
do
    echo "$a - $b"
done

while 命令

1
2
3
4
while test command
do
	commands
done

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var1=10
while [ $var1 -gt 0 ]
do
    echo $var1
    var1=$(( var1 - 1 ))
done
echo

var1=5
# 多个测试命令
# 只有最后一个命令成立时才会停止。如果互换了下面两条命令的位置,就一直循环下去了
while echo $var1
        [ $var1 -gt 0 ]
do
    echo "This is inside the loop"
    var1=$(( var1 - 1 ))
done

until 命令

1
2
3
4
until test command
do
	commands
done

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var1=100

until [ $var1 -eq 0 ]
do
    echo $var1
    var1=$(( var1 - 25 ))
done
echo

# 使用多个测试命令
var1=100
until echo $var1
        [ $var1 -eq 0 ]
do
    echo Inside the loop.
    var1=$(( var1 - 25 ))
done

break 和 continue

和编程语言中的 break 和 continue 类似,不同的是,在 shell 中它们后面可以跟一个数字,指定跳出的层数。

1
2
3
4
5
6
7
8
9
10
11
# 注意在 while 中使用 continue,下面的例子不会结束
var2=1
while [ $var2 -lt 15 ]
do
    if [ $var2 -gt 5 ] && [ $var2 -lt 10 ]
    then 
        continue
    fi
    echo "The value is $var2"
    var2=$((var2+1))
done

处理循环的输出

可以在 done 命令之后添加一个处理命令。

1
2
3
4
5
6
7
8
# 在 done 后面使用管道或重定向命令
for state in "North Dakata" Connecticut Illinois Alabama Tennessee
do
    echo "The state is $state"
done | sort
# 或者
# done > test.txt

处理用户的输入

常用参数变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
$0 脚本名,带有路径
# 使用 basename 只获取名称
$(basename $0)

$1 ... $9, ${10}, ${11} ...

# 在使用参数前注意检查是否存在
if [ -n "$1" ]
then
	...
fi

# 特殊参数变量
$# 获取参数的个数

${!#} 获取最后一个参数。如果没有参数,${!#} 返回的是脚本的名称

# 这种方法也是返回参数的个数,如果没有参数,返回 0
last=$#
echo "The parameter number is $last"

$* 把所有的输入当作一个整体

$@ 会分割输入的参数,可使用 for 循环分别获取

for param in "$@"
do
	...
done

shift 命令移动变量

作用:将每个参数变量向左移动一个位置。变量 $0 的值不会变。

如果某个参数被移除,它的值就被丢弃了,无法再恢复。

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash

# shift 后面也可以跟参数,表示一次移动多个
count=1
while [ -n "$1" ]
do
    echo "Parameter #$count = $1"
    count=$((count+1))
    shift
done

# 使用 shift 之后,就无法再获取参数的值了
echo "The second parameter is $2"

处理选项

命令行选项后面可以带参数;

shell 会用双破折号来表明选项列表结束,在双破折号之后,就可以将剩下的命令行参数当作参数;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
while [ -n "$1" ]
do
    case "$1" in
        -a) echo "Found the -a option" ;;
        -b) echo "Found the -b option, with parameter value $2"
            shift ;;
        -c) echo "Found the -c option" ;;
        # 使用 -- 把命令选项和参数分隔开
        --) shift
            break;;
        *) echo "$1 is not an option";;
    esac
    # 移动变量
    shift
done

# 读取参数
count=1
for param in "$@"
do
    echo "Parameter #$count: $param"
    count=$((count+1))
done

# 测试
$ sh testsh.sh -a -b 23 -c -d -- param1 param2

弊端,将多个选项放在一起就不能用了,比如 -ab

使用 getopt

getopt optstring parametrs

optstring 中列出要在脚本中用到的每个命令行选项字母,在每个需要参数值的选项字母后加一个冒号。

1
2
3
$ getopt ab:cd -a -b test1 -cde test2 test3
getopt: illegal option -- e
 -a -b test1 -c -d -- test2 test3

如果指定了一个不在 optstring 中的选项,会抛出错误信息,使用 -q 选项可以忽略。

在脚本中使用 getopt 命令:

set 命令的选项之一是双破折号(—),它会将命令行参数替换成 set 命令的命令行值。

1
2
3
4
5
# 使用 getopt 处理命令参数
# getopt 处理带空格的参数有问题
set -- $(getopt -q ab:cd "$@")

# 剩下的和上面的处理一样

弊端,处理带空格的参数值有问题。

使用 getopts

getopts optstring variable

每次调用它时,只处理命令行上检测到的一个参数。处理完所有的参数之后,会退出并返回一个大于 0 的退出状态码。

要去掉错误消息的话,可以在 optstring 之前加一个冒号。

用到两个环境变量。如果选项需要跟一个参数值,OPTARG 环境变量就会保存这个值。OPTIND 环境变量保存了参数列表中 getopts 正在处理的参数位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#!/bin/bash

echo
while getopts :ab:c opt
do
    case "$opt" in
        # 这边不需要单破折号
        a) echo "Found the -a option" ;;
        b) echo "Found the -b option, with value $OPTARG" ;;
        c) echo "Found the -c option" ;;
        *) echo "Unknown option: $opt"
    esac
done

# 移动参数
shift $((OPTIND - 1))

count=1
for param in "$@"
do
    echo "Parameter #$count: $param"
    count=$((count+1))
done


# 使用示例
# 选项可以放在一起
$ ./test19.sh -ab test1 -c

# 参数中可以有空格
$ ./test19.sh -ab "test test" -c

# 参数和选项不用空格分开
$ ./test19.sh -abtest

# 会将没有定义的选项输出为问号


将选项标准化

有些字母选项在 Linux 世界里已经有了某种程度的标准含义。

获取用户输入 read

使用 read 命令:-p -s -n -t

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
read name
echo "Your name is $name"

# 使用 -p 参数
read "Please input your name: " name
echo "Your name is $name"

# read 一次性读入多个变量
read -p "please input two number: " a b
echo "The sum of the two numbers is: $((a+b))"

# 使用 -s 参数,隐藏输入内容
read -s -p "Please input your password: " password
echo "Is your password really $password?"

# read 后面不指定变量,会放在 REPLY 环境变量中
read -p "Enter your name: "
echo "Your name is $REPLY"

# 使用 -t 指定等待的秒数
if read -t 5 -p "Please enter your name: " name
then
    echo "Hello $name"
else
    echo "Sorry, too slow!"
fi

# 使用 -n 指定输入的字符数。输入的字符数达到指定的值,就会自动回车完成输入
read -n1 -p "Do you want to continue [Y/N]? " answer
case $answer in
Y | y) echo
        echo "fine, continue on...";;
N | n) echo
        echo "Ok, goodbye"
        exit;;
esac

读取文件:

1
2
3
4
5
6
7
8
count=1
# 每次从文件中读取一行
cat states | while read line
do
    echo "Line $count: $line"
    count=$((count+1))
done
echo "Finished processing the file"

呈现数据

Linux 系统将每个对象当作文件处理。Linux 用文件描述符(file descriptor)来标识每个文件对象。

每个进程一次最多可以有九个文件描述符。bash shell 保留了个前三个(0、1、2)。

0 STDIN,标准输入;

1 STDOUT,标准输出;

2 STDERR,标准错误;

当在命令行上只输入 cat 命令时,它会从 STDIN 接受输入。输入一行,cat 命令就会显示出一行。

1
2
3
4
5
6
7
8
9
10
# 只重定向错误
$ ls -la badfile 2> errorfile

# 重定向错误和数据
$ ls -la test test2 badfile 2> errorfile 1> outfile

# 重定向错误和数据到一个文件
$ ls -la test test2 badfile &> outfile
# 注意,为了避免错误信息散落在输出文件中,相较于标准输出,bash shell 自动赋予了错误消息更高的优先级。
# 所以输出文件中,错误信息集中显示在最开头

在脚本中重定向输出、输入

在重定向到文件描述符时,必需在文件描述符数字之前加一个 &

1
2
echo "This is an error msg" >&2
echo "This is a normal msg"

如果脚本中有大量数据需要重定向,重定向每个 echo 语句就很烦琐。可以使用 exec

1
2
3
4
5
6
7
8
9
10
exec 1>testout

echo "This is a test of redirecting all outoupt"
echo " from a script to another file"
echo "This is an other msg" >&2

exec 2>testerror

echo "This is an other normal msg"
echo "This is an other msg2" >&2

重定向输入:

1
2
3
4
5
6
7
8
9
exec 0< states

count=1

while read line
do
    echo "State #$count: $line"
    count=$((count+1))
done

创建自己的重定向

其它6个3~8的文件描述符可用作输入或输出重定向。

1
2
3
4
5
6
exec 3>test28out
# 当然也可以追加
# exec 3>>test28out

echo "1. This should display on the monitor"
echo "and this should be stored in the file" >&3

一旦重定向了 STDOUT 或 STDERR,怎么再切回来呢?

1
2
3
4
5
6
7
8
9
10
11
12
# 在脚本中临时重定向输出,然后恢复默认输出设置
# 3->monitor
exec 3>&1
echo "2. This should display on the monitor"
# 1->file
exec 1>test28out2
echo "This should be stored in the file"
echo "3. This should display on the monitor" >&3

# 1->monitor
exec 1>&3
echo "4. This should display on the monitor"

同样的方法用在重定向输入中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/bash

# 保存 STDIN,之后再恢复
exec 6<&0

exec 0< states

count=1
while read line
do
    echo "State #$count: $line"
    count=$((count+1))
done

exec 0<&6
read -p "Are you done now? " answer
case $answer in
    Y|y) echo "Goodbye";;
    N|n) echo "Sorry, this is the end";;
esac

创建读写文件描述符,即打开单个文件描述符来作为输入和输出,注意,任何读或写都会从文件指针上次的位置开始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/bash

# 创建读写文件描述符

exec 3<>testfile

# 读第一行
read line <&3
echo "Read: $line"
# 此时文件指针在第二行的开头
echo "This is a test line" >&3

#testfile
# This is the first line.
# This is the second line.
# This is the third line.

# 运行结束查看 testfile
# This is the first line.
# This is a test line
# ine.
# This is the third line.

关闭文件描述符 exec 3>&-,关闭之后就不能使用了。不知为何在 mac 上没有效果:

1
2
3
4
5
6
7
8
9
10
#!/bin/bash

exec 3>test31file
echo "This is a test line of data" >&3

# 关闭文件描述符
# mac没有生效
echo 3>&-

echo "This won't work" >&3

如果在关闭之后,随后在脚本中打开了同一个输出文件,会用一个新文件来替换已有文件。

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash

exec 3>test32file
echo "This is a test line of data" >&3
exec 3>&-

cat test32file

# 会覆盖上面的文件
exec 3>test32file
echo "This'll override file" >&3

列出打开的文件描述符 lsof

lsof 会列出整个 Linux 系统打开的所有文件描述符。

在很多 Linux 系统中,lsof 命令位于 /usr/sbin 目录,要想以普通用户账号来运行,必需通过全路径名来引用:/usr/sbin/lsof

-p 指定进程ID(PID)

-d 指定要显示的文件描述符编号

-a 用来对其它两个选项的结果执行布尔 AND 运算

$$ 环境变量,当前进程的PID

1
2
3
4
5
$ lsof -a -p $$ -d 0,1,2
COMMAND   PID    USER   FD   TYPE DEVICE  SIZE/OFF NODE NAME
zsh     45488 xiaocan    0u   CHR   16,0 0t1828098 4717 /dev/ttys000
zsh     45488 xiaocan    1u   CHR   16,0 0t1828098 4717 /dev/ttys000
zsh     45488 xiaocan    2u   CHR   16,0 0t1828098 4717 /dev/ttys000

FD 文件描述符以及访问类型(r-read,w-write,u-读写)

TYPE 文件的类型(CHR 代表字符型,BLK 代表块型,DIR 代表目录,REG 代表常规文件)

与 STDIN、STDOUT 和 STDERR 关联的文件类型是字符型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/bash

exec 3> test33file1
exec 6> test33file2
exec 7< states

/usr/sbin/lsof -a -p $$ -d 0,1,2,3,6,7,8

# 结果都是 REG 类型的,代表它们都是常规的文件
$ sh test33-lsof.sh
COMMAND   PID    USER   FD   TYPE DEVICE SIZE/OFF       NODE NAME
bash    48443 xiaocan    0u   CHR   16,1  0t22389       5055 /dev/ttys001
bash    48443 xiaocan    1u   CHR   16,1  0t22389       5055 /dev/ttys001
bash    48443 xiaocan    2u   CHR   16,1  0t22389       5055 /dev/ttys001
bash    48443 xiaocan    3w   REG    1,4        0 8635172069 /Users/xiaocan/Documents/learn/shell-learn/test33file1
bash    48443 xiaocan    6w   REG    1,4        0 8635172070 /Users/xiaocan/Documents/learn/shell-learn/test33file2
bash    48443 xiaocan    7r   REG    1,4       50 8634917711 /Users/xiaocan/Documents/learn/shell-learn/states

阻止命令输出

将文件重定向到 /dev/null 文件。

可以用它来清空日志文件:cat /dev/null > logfile

创建临时文件

系统上任何用户都有权限读写 /tmp 目录中的文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# mac 上的运行结果
$ mktemp
/var/folders/gc/j2xc0rlx5bx4frx_54gb6sbr0000gn/T/tmp.olLTfDFA

$ mktemp testing.XXXXXX
testing.t3DvSz

# 只有属主用户有权限
$ ls -l testing.t3DvSz
-rw-------  1 xiaocan  staff  0 Feb 13 20:15 testing.t3DvSz

# -t 参数指定在临时文件夹中创建文件
$ mktemp -t testing.XXXXXX
/var/folders/gc/j2xc0rlx5bx4frx_54gb6sbr0000gn/T/testing.XXXXXX.B5qQrUge

# -d 参数创建临时文件夹
$ mktemp -d 


# 在脚本中使用
# tempfile=$(mktemp test34.XXXXXX)

记录消息 tee

同时重定向到控制台和文件中。

1
2
3
4
5
6
$ date | tee testfile

echo "test message" | tee testfile

# 使用 -a 追加
echo "test message" | tee -a testfile

函数

创建函数

1
2
3
4
5
6
7
8
9
function name {
	commands
}

# 或者

name() {
	commands
}

注意,函数必需先定义,然后才能使用。

使用举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/bash

# 使用函数

function func1 {
    echo "This is an example of a function"
}

count=1
while [ $count -le 5 ]
do
    func1
    count=$((count+1))
done

# 会覆盖上面的定义
function func1 {
	echo "This is another example"
}

func1

返回值

有三种方式获取:

第一,默认最后一个命令的退出状态码;

1
2
3
4
5
6
7
8
9
#!/bin/bash
# 不推荐使用
func1() {
    echo "trying to display a non existent file"
    ls -l badfile
}

func1
echo "The exit status is: $?"

第二,使用 return 命令;

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
# 使用 return 命令
# 有两点需要注意:
# 1. 函数一结束就取返回值
# 2. 退出状态码必须是 0~255
function db1 {
    read -p "Enter a value: " value
    echo "doubling the value"
    return $((value * 2))
}
db1
echo "The new value is $?"

第三,使用函数输出,推荐使用;

1
2
3
4
5
6
7
8
9
#!/bin/bash

function db1 {
    read -p "Enter a value: " value
    echo $((value * 2))
}
# 命令替换
result=$(db1)
echo "The new value is $result"

向函数传递参数

同脚本获取命令行参数一样,函数也使用 $1$9 … 获取参数。$0 是函数名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/bash

# 向函数传递参数

function addem {
    if [ $# -eq 0 ] || [ $# -gt 2 ]
    then
        echo -1
    elif [ $# -eq 1 ]
    then
        echo $(($1 + $1))
    else
        echo $(($1 + $2))
    fi
}

echo "Adding 10 and 15: $(addem 10 15)"

在函数中使用变量

默认情况下,在脚本中定义的变量都是全局变量,在函数内外都可以使用。

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash

# 默认都是全局变量

function db1 {
    value=$((value * 2))
}

read -p "Enter a value: " value
db1
echo The new value is: $value

声明局部变量,在变量前加上 local 关键字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/bash

# 定义局部变量

function func1 {
    local temp=$((value+5))
    result=$((temp * 2))
}

temp=4
value=6

func1
# 22
echo "The result is $result"
# 4
echo "The temp value is $temp"

向函数传递数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/bin/bash

# 给函数传递数组

wrong() {
    echo "The parameters are: $@"
    thisarray=$1
    echo "The received array is ${thisarray[*]}"
}

myarray=(1 2 3 4 5)
echo "The origin array is ${myarray[*]}"
# 这种传递数组的方式不行
wrong $myarray
echo

# 正确使用方法
right() {
    echo "The parameters are: $@"
    # 在内部创建一个新的变量
    newarray=($(echo $@))
    echo "The received array is ${newarray[*]}"
}

right ${myarray[*]}

# 结果是
# The origin array is 1 2 3 4 5
# The parameters are: 1
# The received array is 1
# 
# The parameters are: 1 2 3 4 5
# The received array is 1 2 3 4 5

函数返回数组

递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/bash

# 递归
# 计算一个数的阶乘

function factorial {
    if [ $1 -eq 1 ]
    then
        echo 1
    else
        local temp=$(($1 - 1))
        local result
        result=$(factorial $temp)
        echo $((result * $1))
    fi
}

read -p "Enter value: " value
result=$(factorial $value)
echo "The factorial of $value is: $result"

创建库

创建库文件,然后在多个脚本中引用,方便函数重用。

示例库函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
cat myfuncs 
# my script functions

function addem {
    echo $(($1 + $2))
}

function multem {
    echo $(($1 * $2))
}

function divem {
    if [ $2 -ne 0 ]
    then
        echo $(($1 / $2))
    else
        echo -1
    fi
}

和环境变量一样,shell 函数仅在定义它的 shell 会话内有效。在命令行中运行 myfuncs 脚本,shell 会创建一个新的 shell 运行它,之后在当前的 shell 中也无法使用它:

1
2
3
$ ./myfuncs 
$ addem 1 2
bash: addem: command not found

source 命令会在当前 shell 上下文中执行命令,而不是创建一个新 shell。source 命令有一个快捷的别名,称作点操作符(dot operator)

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash
# 使用库函数

# 这种方式不正确
# ./myfunc

# 使用这种方式执行
. ./myfuncs

result=$(addem 10 15)
echo "The result is $result"

在命令行上使用库函数

在命令行上创建函数:

1
2
3
4
5
6
7
8
9
$ function divem { echo $(($1 / $2));}
$ divem 5 2
2

$ function multem {
> echo $(($1 * $2))
> }
$ multem 2 5
10

.bashrc 中定义函数,或者使用点操作符加载库函数。这样就可以在 shell 中是使用了。

控制脚本