shell 随笔

1、man
manual 是类 UNIX 系统的标准手册工具,共 9 卷,用 man man可以看到 9 卷分别是什么内容:

   1   Executable programs or shell commands 可执行程序或shell命令
   2   System calls (functions provided by the kernel)  系统调用
   3   Library calls (functions within program libraries) 库调用
   4   Special files (usually found in /dev) 特殊文件
   5   File formats and conventions eg /etc/passwd 文件格式和约定
   6   Games 游戏
   7   Miscellaneous (including macro packages and conventions), e.g. man(7), groff(7) 杂项(包括宏和惯例)
   8   System administration commands (usually only for root) 系统管理命令(通常仅适用于root用户)
   9   Kernel routines [Non standard] 内核例程(非标准)

如果只有一个含义,直接查询即可,比如man ls,但有很多是一词多义的:chmod, chown, chmod, stat, exit, info, socket, flock, open, write, printf, passwd 等等。
man 1 printf 表示 shell 命令,
man 3 printf 表示 C 语言标准库函数,
可以用 -a 选项显示全部,比如 man -a printf ,按 q 退出之后,可以按回车键表示继续看下一卷,或按 Ctrl-C 直接退出。

2、#!/bin/bash
#!/bin/bash放在 shell 脚本开头,是指此脚本使用/bin/bash来解释执行。

#!/usr/bin/python 也是类似的, 表示用/usr/bin/python来解释执行 python 脚本,加了这行就可以用./xx.py的方式来运行了。

#!/usr/bin/env python 会去环境变量路径中寻找 python 目录。

3、shell 运行方式

(1) bash 1.sh
创建子进程运行 1.sh,需要有 r 权限。
(2) ./1.sh
创建子进程运行 1.sh,需要有 rx 权限。
(3) source 1.sh
当前进程运行 1.sh,需要有 r 权限,
(4) . 1.sh
点命令是 source 命令的另一种写法。

如何证明是在当前进程运行,还是创建子进程运行呢,下面用 2 种简单的方法:
(1) 创建一个子目录,然后用 cd 命令

~/test$ mkdir child
~/test$ echo 'cd child;pwd' > 1.sh

使用 bash 执行结束后,可以看左边的提示符,当前的工作目录没有改变,cd 命令只对子进程产生影响:

~/test$ bash 1.sh
/home/oo/test/child
~/test$ 

使用 source执行结束后,当前目录变成了 child 目录,因为 cd 命令对当前进程产生了影响:

~/test$ source 1.sh
/home/oo/test/child
~/test/child$ 

(2) 变量赋值
bash是创建子进程来执行脚本,所以子进程的变量,在父进不能访问;
source在当前进程执行,所以脚本中对变量的赋值,当前进程能访问:

$ echo "a=99" > 1.sh
$ bash 1.sh 
$ echo $a

$ source 1.sh
$ echo $a
99

4、内部命令,外部命令
通过 type 命令可以查询某个命令是内部命令、外部命令,还是别名(alias):

$ type cd
cd is a shell builtin
$ type find
find is /usr/bin/find
$ type ls
ls is aliased to `ls --color=auto'

如果用 bash shell,可以通过help命令列出所有内部命令列表。

区别:
(1)内部命令不创建子进程,对当前进程有影响,执行速度快;
外部命令不在shell中,保存在一个独立的文件中,会创建新的进程来执行。
(2)内部命令查询帮助信息用:help 内部命令,比如 help cd
外部命令查询帮助信息用:外部命令 --help,比如 ls --help

当然除了 manhelp 之外,info 也可以用于查询帮助信息。

5、重定向

(1) 输入重定向 <

$ wc -l < /etc/passwd
32
$ echo 123 > 1.txt
$ read var < 1.txt 
$ echo $var
123

(2) 输出重定向
① > 先清空该文件,再追加内容。
② >> 文件的内容不变,在文件末尾追加内容。
③ 2> 错误重定向

$ nothing
nothing: command not found
$ nothing 2> 1.txt
$ cat 1.txt 
nothing: command not found

④ &> 表示无论是正确还是错误,都输出重定向。

(3) 输入、输出重定向的组合使用

将开始标记 和结束标记之间的内容作为 cat的输入,cat 的输出重定向到文件 1.txt:

$ cat > 1.txt << EOF
> 123
> 456
> EOF
$ cat 1.txt 
123
456

这里的 EOF 只是开始和结尾的 tag,也可以换成其他的,比如ABCD:

$ cat > 1.txt << ABCD
> 1234
> 5678
> ABCD
$ cat 1.txt 
1234
5678

6、变量
(1) 赋值, = 的两边不能有空格,否则被解释为命令:

$ a=123
$ echo $a
123
$ a =123
a: command not found
$ a= 123
123: command not found

(2) 算数运算用 let ,否则默认是字符串

$ a=1+2+3
$ echo $a
1+2+3
$ let a=1+2+3
$ echo $a
6

(3) 把命令赋值给变量,意义不大:

$ a=ls
$ $a -l

(4) 把命令的结果赋值给变量:

$ a=$(find . -name *.sh)
$ echo $a

(5) 作用范围
变量只在当前 shell 生效,子shell、父shell 都不能共用。
使用 export 命令之后,子shell 可以获取 父shell 的变量。其他终端不可以。

(6)变量不用了之后,可以删除: unset 变量名。

7、环境变量

(1)env 命令可以列出所有环境变量,一般是大写。

PATH 指定命令搜索路径,
PS1 可以使当前终端显示更友好,
UID 用户ID,
USER 用户名。
PWD 当前路径,
LD_LIBRARY_PATHLD_PRELOAD 程序运行时,搜索动态库路径。

(2) set 命令可以列出系统中已经存在的 shell 变量,比 env 命令列出的变量更多。

$ abcde=1234567
$ set | grep abcde
abcde=1234567

(3) 预定义变量
$$ :当前进程的PID号
$0 :当前程序或脚本的名称
$? :用于返回上一条命令是否成功执行。如果成功执行,将返回数字0,否则返回非零数字(通常情况下都返回数字1)。
$*$@ :保存传递给脚本或进程的所有参数
$# :脚本参数的个数,(类似C语言的 argc)
$1$2${10} 等等 :位置变量,从命令行给脚本传参(类似C语言的argv[]),还有函数传参时会使用。

(4) 环境变量的配置文件
/etc/profile 是所有用户共用的。
~/.bashrc 是某一个用户特有的。
如果存在重复定义的变量,/etc/profile 先执行,会被后执行的 ~/.bashrc 覆盖。万一有变量被重复定义了,执行顺序很重要,可以在这 2 个文件中添加echo 输出重定向到文件,来判断谁先被执行。

另外:
su root 是 non-login shell,不会加载所有的配置文件,不建议使用;
su - root 是 login shell,会加载所有配置文件,推荐使用这种方式。

8、数组
定义数组用小括号,例如: a=(1 2)
访问所有元素 ${a[@]}
数组元素的个数 ${#a[@]}
访问单个元素 ${a[0]},负号表示倒数第几个 ${a[-1]}

$ a=( 11 22 33 )
$ echo ${a[@]}
11 22 33
$ echo ${a[*]}
11 22 33
$ echo ${#a[@]}
3
$ echo ${a[0]}
11
$ echo ${a[1]}
22
$ echo ${a[-1]}
33
$ echo ${a[-3]}
11

9、特殊字符
# 注释
; 分号用来分隔两条命令。
` 反引号 与 $() 都是用来执行命令的。
" 双引号(不完全引用)会对变量解释,
' 单引号(完全引用)不解释变量。

$ a=hello
$ echo "$a"
hello
$ echo '$a'
$a

10、整数运算
默认会解释为字符串,所以需要用 expr,注意空格:

$ expr 1 + 2 + 3
6
$ expr 1+ 2 + 3
expr: syntax error

除了 + 还有 - * / % > < 等等,注意转义字符:

$ expr 1 - 2
-1
$ expr 6 \* 6
36
$ expr 30 / 6
5
$ expr 32 % 3
2
$ expr 3 \> 4
0
$ expr 3 \< 4
1
$ expr 3 \<= 3
1
$ expr 3 \!= 3
0
$ expr 3 \!= 4
1

不支持小数运算:

$ expr 5 + 0.5
expr: non-integer argument

另外 length 可以计算字符串长度:

$ expr length "hello"
5

**表示乘方,expr没有乘方功能:

$ let a=2**3
$ echo $a
8

let 简写为双圆括号:

$ ((a=5+6))
$ echo $a
11

11、园括号()里面的赋值,不会影响外面。

$ a=10;echo $a
10
$ (a=20;echo $a)
20
$ echo $a
10

是圆括号()创建了一个子进程吗,还是其他原因呢?
可以做一个小实验,先打开 2 个终端:
先查看有几个 bash 进程,可以看到是 5 个:

$ ps aux |grep bash |wc -l
5
$ (a=20;echo $a;sleep 9)

把圆括号睡眠一下,趁这个时间再次查看 bash 数量,比刚才多了 1 个,等 sleep 结束之后再看 bash 数量又恢复 5个,说明圆括号()确实创建了子进程:

$ ps aux |grep bash |wc -l
6

另外也可以再通过 cd 命令来看,圆括号中改变了工作目录,并不影响父进程:

$ pwd
/home/oo/test
oo@uu:~/test$ (a=20; cd ..; pwd)
/home/oo
oo@uu:~/test$ pwd
/home/oo/test

12、test 命令
test 命令是通过设置 $? 的值, 来让我们可以判断是否正常退出,或者条件是否为真,0 为 真,非 0 为假。

test 通常有三类测试:字符串、数字、文件,可以通过 man 查询具体用法:
man test|grep INTEGER
man test|grep STRING
man test|grep FILE

通过 man test 可以看到 test EXPRESSION[ EXPRESSION ] 两种写法,
有时为了程序美观,可以把 test 换成方括号[ ] 的写法。

(1) 字符串
字符串是否相等,会区分大小写:

$ [ "abc" = "abc" ]
$ echo $?
0
$ [ "abc" = "a" ]
$ echo $?
1

(2) 整数
[] 扩展为 [[]] ,可以支持更多运算符。

$ test 3 -gt 4
$ echo $?
1
$ [ 3 -gt 4 ]
$ echo $?
1
$ [[ 3 > 4 ]]
$ echo $?
1

(3) 文件
文件是否存在:

$ test -f /etc/passwd
$ echo $?
0
$ test -f /etc/passwd.1234
$ echo $?
1

目录存在:

$ test -d /etc
$ echo $?
0

文件或目录存在:

$ test -e /etc
$ echo $?
0
$ test -e /etc/passwd
$ echo $?
0

13、大括号
(1) 表示某个范围内的值,主要是给 for 循环使用的

$ echo {0..3}
0 1 2 3

(2) 减少重复书写
cp -v /etc/passwd{,.bak} 相当于 cp -v /etc/passwd /etc/passwd.bak

14、if
(1)if-then-fi
(2)if-then-else-fi
(3)if-then-elif-then-else-fi

let a=1-6
if [ $a -eq 0 ];
then
    echo "a=0"
elif [ $a -lt 0 ];
then
    echo "a<0"
else
    echo "a>0"
fi

15、case-esac

a=3
case "$a" in
1)
echo "case 1"
;;
2|3)
echo "case 2 or 3"
;;
*)
echo "case *"
;;
esac

case 语句的例子在很多文件都有,比如 grep -n case ~/.bashrc

16、for

for i in {1..3}
do
    echo $i
done
for filename in /etc/profile.d/*.sh
do
    if [ -x $filename ]; then
        source $filename;
        echo "source $filename;";
    fi
done

下面是 C 语言风格的写法:

for ((i=1;i<=3;i++))
do
    echo $i
done

17、while

a=1
while [ $a -le 3 ];
do
    echo $a;
    ((a++));
done

用冒号表示条件为真,写一个死循环:

while :
do
    echo "always"
done

18、until
until 循环与 while 语法一致,只是条件为假才执行循环:

a=8
until [ $a -lt 5 ];
do
    echo $a;
    ((a--));
done

19、break 与 continue

for n in {1..5}
do
    if [ $n -eq 3 ];then
        break;
    fi
    echo $n
done
for n in {1..5}
do
    if [ $n -eq 3 ];then
        continue;
    fi
    echo $n
done

20、字符串截取命令
basename 命令,可以用来删掉后缀名:

$ basename hello.c.diff .diff
hello.c
$ basename hello.c.diff .c.diff
hello

dirname用于获取目录

$ dirname /etc/hosts
/etc

其实它们不在乎这个目录是否真实存在,只是简单的处理一下字符串而已:

$ dirname /1/2/3
/1/2

21、函数
函数定义的格式:
函数名(){ }

$ testfun(){ echo hello;};testfun;
hello

避免函数内部的变量影响到外面,使用 local 关键字限定作用域为该函数。

你可能感兴趣的:(shell 随笔)