一、基础认知
为何需要 shell:DevOps 是避不开编程的,以后的运维也是,大势所趋。
1. Linux 底层简单介绍(shell 译为壳)
实现 shell 的程序称为 bash。
需要说明:操作系统是一个整体,内核和 shell 都是它的两个部分。

shell 的作用就是解释执行用户输入的命令或程序,用户输入一条它就解释一条,这种键盘输入、Linux 给响应的方式称为交互式。
命令执行过程:

早期是编写机器语言,编译后运行的,直到解释器的出现。
/usr/bin 下的文件属于二进制文件,打开都是无法阅读的。
2. Shebang
和 shell 的交互可以是非交互式的,代码写到文件,用的时候运行(比如 Linux 的 .sh 文件、win 的 .bat 文件),称为 shell 脚本(非交互式)。
也可以不写后缀,但俗成的东西没必要破坏。
脚本的首行会有 #!(称为 Shebang)和路径,即说明信息,使用 shell 语法编写。
Shebang 的作用是指明解释器去读取,即使脚本不在 /bin 目录,写 Shebang 也可使用 /bin/sh,让指向的解释器执行。
/bin/sh 是一个可执行文件。

yum 就是用 python 开发的,要用 python 解释器。
env 的写法是兼容的写法。
ubuntu@VM-0-12-ubuntu:/etc/init.d$ cat /etc/init.d/procps
#! /bin/sh
DESC="Setting kernel variables"
DAEMON=/sbin/sysctl
PIDFILE=none
ll /bin/sh
lrwxrwxrwx 1 root root 4 Mar 23 2022 /bin/sh -> dash*
# 也有bash
ll /bin/bash
-rwxr-xr-x 1 root root 1396520 Mar 14 2024 /bin/bash*
# python解释器分别是centos和Ubuntu的
#! /usr/bin/python
#! /usr/bin/python3
# 兼容解释器
#! /usr/bin/env
习惯上使用 bash 即可。
命令行执行的都会去 PATH 里找,不写入 PATH 的用相对路径执行。
有些系统在创建文件时可能缺少执行权限,此时可以 chmod +x 文件名 来添加,也可以使用临时解释器(即指定解释器):
root@VM-0-9-ubuntu:~/shell# sh hello.sh
server-test
root@VM-0-9-ubuntu:~/shell# bash hello.sh
server-test
root@VM-0-9-ubuntu:~/shell# dash hello.sh
server-test
root@VM-0-9-ubuntu:~/shell# /bin/sh hello.sh
server-test
root@VM-0-9-ubuntu:~/shell# /bin/dash hello.sh
server-test
root@VM-0-9-ubuntu:~/shell# /bin/bash hello.sh
server-test
# 有很多指定执行方式,随便选,第一种最简单的
# 不写./hello.sh也可以,因为指定解释器,就会在当前目录搜索
# 不指定解释器,就回去PATH搜索因此下面是不行的:必须加./
root@VM-0-9-ubuntu:~/shell# chmod +x hello.sh
root@VM-0-9-ubuntu:~/shell# hello.sh
hello.sh: command not found
root@VM-0-9-ubuntu:~/shell# ./hello.sh
server-test
# 养成一个./的习惯是好事
如果是一个 py 文件,只加可执行权限是没用的,因为默认是 bash 解释器执行的,需要指定一个 py 解释器,/usr/bin/python3 文件名.py 这样指定就行。
结论: py 文件必须指定 py 解释器,也可以写 py 的 shebang。
root@VM-0-9-ubuntu:~/shell# vim hello.py
root@VM-0-9-ubuntu:~/shell# cat hello.py
#! /usr/bin/python3
print("server-测试")
root@VM-0-9-ubuntu:~/shell# chmod +x hello.py
root@VM-0-9-ubuntu:~/shell# ./hello.py
server-测试
root@VM-0-9-ubuntu:~/shell# hello.py
hello.py: command not found
查看 shell 的值 echo $SHELL:
echo $SHELL
/bin/bash
python3 解释器基本解决中文乱码问题(# conding:utf-8 不需要写了)。
如果 Shebang #! 指定的不是一个可执行文件,最终会交给 shell。
3. 浅谈运维和编程
注释规范
注释一般要求单独写一行,也可以在命令后面:
#! /bin/bash
#
echo "server-test" #注释在这里

最好使用英文而非中文(在公司写中文也没问题,只是会涉及系统平台语言不兼容问题)。
执行 shell 脚本的方式
- 用 bash/sh 指定文件的绝对路径、相对路径运行(需要
x执行权限)
白色的就是没有执行权限,绿色才有:

source hello.sh、. hello.sh的方式,具体区别后面了解- 少见:
sh < hello.sh

简单来说就是随便定义变量类型不用声明。
选定了学习路径就要坚定,别人说好的不跟风,别人说不好的不迷茫,犹犹豫豫走走停停一定会暴毙。
二、一阶段:bash 特性和变量
1. bash 特性
history 介绍
shell 会话可以保留用户执行的历史命令 history,错误的、正确的都会有。
echo $HISTSIZE 这个变量定义了历史命令的数量:
root@VM-0-9-ubuntu:~/shell# echo $HISTSIZE
1000
记忆:hist size。
历史命令是记录在文件中的:cat ~/.bash_history、cat /root/.bash_history。
可以通过 Tab 补全输入。
history 常用参数
history -c清空历史history -r ~/.bash_history恢复历史
!3006 执行历史命令,!! 执行上次命令(执行长命令用到,也可以通过上下左右箭头用到)。
bash 默认支持这些功能:文件路径命令补全、ctrl + a、e、u、k、l、通配符、history、命令别名、命令行展开。
2. Shell 变量详解
赋值和输出:
# 临时变量,更新会话、重启都会消失
root@VM-0-9-ubuntu:~/shell# name="嘿嘿嘿"
root@VM-0-9-ubuntu:~/shell# echo $name
嘿嘿嘿
root@VM-0-9-ubuntu:~/shell# sudo -i
root@VM-0-9-ubuntu:~# echo $name
root@VM-0-9-ubuntu:~#
很早之前就说过变量,把右边赋值给左边,但是本次会深入了解一些变量。
定义变量和赋值之间,不得有空格(加引号是好习惯)。
单引号变量不识别特殊语法,双引号变量能识别特殊符号:

每次调用 bash/sh 解释器执行脚本都会开启子 shell,因此不保留当前 shell 的变量。
bash make_vars.sh 之后再 echo $name,拿到的还是父 shell 里的旧值:

调用 source 或者标点符号,在 shell 环境加载脚本,保留变量。

反引号(不是单引号)的执行结果会被保留:

此种方式只是存储了执行结果为字符串,与真正的命令输出并不同(比如高亮):


使用 source 命令执行才会输出用户,不产生子 shell。
linux 是 [命令][空格][参数] 操作的对象,无需声明变量类型。
echo $变量名、echo ${变量名} 可以输出,前面是简写。
命名规则

变量作用域


环境变量,也是全局变量,在所有的 shell 都可以找到,在父 shell 和子 shell 都能找到。
位置参数变量:ls -l /opt。
$? 这个值代表上一次的执行结果,0 就成功,其他就是失败了。
3. 通过 set 寻找父子 shell 的变量加载
环境变量
PATH 变量是环境变量,能找出 linux 服务器的各种变量:
root@VM-0-9-ubuntu:~/shell# echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
root@VM-0-9-ubuntu:~/shell# which ls
/usr/bin/ls
$PATH 保存了很多路径,内核从路径找到 ls 然后执行。
想要一个变量永久生效,就要修改环境变量配置文件,把加入的变量改为环境变量。
用户个人配置文件
centos 是 ~/.bash_profile,ubuntu 是 ~/.profile:
root@VM-0-9-ubuntu:~/shell# cat ~/.profile
# ~/.profile: executed by Bourne-compatible login shells.
if [ "$BASH" ]; then
if [ -f ~/.bashrc ]; then
. ~/.bashrc
fi
fi
mesg n 2> /dev/null || true
ubuntu@VM-0-9-ubuntu:~$ cat ~/.profile
# if running bash
if [ -n "$BASH_VERSION" ]; then
# include .bashrc if it exists
if [ -f "$HOME/.bashrc" ]; then
. "$HOME/.bashrc"
fi
fi
# set PATH so it includes user's private bin if it exists
if [ -d "$HOME/bin" ] ; then
PATH="$HOME/bin:$PATH"
fi
# set PATH so it includes user's private bin if it exists
if [ -d "$HOME/.local/bin" ] ; then
PATH="$HOME/.local/bin:$PATH"
fi
CentOS 默认创建 ~/.bash_profile,Ubuntu 默认用 ~/.profile,ubuntu 里面自动判断:如果存在 ~/.bashrc 就加载它,然后把用户自己的 ~/bin 和 ~/.local/bin 加入 PATH。
下面是 centos 的输出:

两种系统配置环境变量的方式都是在文件末尾写 export [变量名]="赋值"。
在 ubuntu 的文件末尾加 export zzz="zzz",然后输出测试:
ubuntu@VM-0-9-ubuntu:~$ source ~/.profile # 加载变量
ubuntu@VM-0-9-ubuntu:~$ echo $zzz
zzz
这样切换 shell 也会保留了,但是切换用户不会,因为只在 ubuntu 用户文件配置了。
全局配置文件
/etc/profile 写入变量就是全局变量了(仅 root),而且不用写 export:
root@VM-0-9-ubuntu:~# cat /etc/profile
# /etc/profile: system-wide .profile file for the Bourne shell (sh(1))
# and Bourne compatible shells (bash(1), ksh(1), ash(1), ...).
if [ "${PS1-}" ]; then
if [ "${BASH-}" ] && [ "$BASH" != "/bin/sh" ]; then
# The file bash.bashrc already sets the default PS1.
# PS1='\h:\w\$ '
if [ -f /etc/bash.bashrc ]; then
. /etc/bash.bashrc
fi
else
if [ "$(id -u)" -eq 0 ]; then
PS1='# '
else
PS1='$ '
fi
fi
fi
if [ -d /etc/profile.d ]; then
for i in /etc/profile.d/*.sh; do
if [ -r $i ]; then
. $i
fi
done
unset i
fi
weather="天气很不错"
全局变量和局部冲突,优先局部。
检查系统环境变量
set 输出所有变量:
root@VM-0-9-ubuntu:~# set | wc -l
1941
grep 支持正则,^ 表示以…开头,比如 set | grep ^name 找出以 name 开头的变量名。
如果 ssh 开了俩窗口,每个窗口的 shell 嵌套都独立于另一个窗口的嵌套 shell:

4. Shell 变量加载顺序
set可以找到当前 shell 的所有变量,可以列出函数,所以一个函数也可以看作定义了局部变量(找全变量)env就省略了函数declare结果和set一样(找全变量)export显示和设置环境变量的值(找环境变量)
root@VM-0-9-ubuntu:~# export | grep PATH
declare -x PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin"
unset [变量名] 删除变量。
readonly 只有 shell 结束,只读变量失效:

取所有环境变量的变量名:

切分的时候 awk -F '[ :=]' '{print $3}' 就行,[] 表示用里面的任意一种方式同时切分。

顺手记一下,多条命令可以用分号串成一行,一次敲完:

环境变量文件的加载顺序大致是这样一层层往下走:

最后一步是 /etc/bashrc:

5. Shell 特殊参数变量
参数变量介绍



下面是双引号的版本:
ubuntu@VM-0-9-ubuntu:~$ vim special.sh
#! /bin/bash
echo "特殊变量$0 $1测试"
echo $0 $1
echo "$#测试"
echo $#
echo "$@测试"
echo $@
echo "$*测试"
echo $*
ubuntu@VM-0-9-ubuntu:~$ sh special.sh z j j 666 666 888
特殊变量special.sh z测试
special.sh z
6测试
6
z j j 666 666 888测试
z j j 666 666 888
z j j 666 666 888测试
z j j 666 666 888
单引号和双引号的区别在于变量是否被解析,双引号会被替换为实际的值,单引号则保留原参数。
**$* 和 $@ 的区别**
都表示用户的所有参数,都是空格分隔,但当双引号包裹输出参数,含义就会不同。
[root@chaogelinux shell_program]# cat different.sh
#!/bin/bash
echo "print each param from \"\$*\""
for var in "$*"
do
echo "$var"
done
echo "print each param from \"\$@\""
for var in "$@"
do
echo "$var"
done


反斜杠是转义符,三个斜杠转义了 "$*",下面的同理。

6. Shell 特殊状态变量
状态变量介绍

图中的 $_ 标错了,表示上一次命令传入的最后一个参数。
获取上次命令退出状态 $?:0 成功,非 0 失败。这里先简单理解,学习函数之后会更好地理解。
脚本执行结束了,返回一个数字 id 作为返回值:


获取当前脚本的 PID $$,在脚本末尾加上 echo "当前的脚本 id 是: $$":


获取上次后台执行程序 PID $!。
nohup xxx & 1>/dev/null 让程序在后台运行,不受终端关闭影响,且不输出任何日志:
- nohup:no hang-up,终端关了程序也不停
- &:放到后台运行,不占用当前终端
- 1>/dev/null:把标准输出(1 是文件描述符,代表标准输出)丢进 /dev/null 黑洞,不显示任何日志
ubuntu@VM-0-9-ubuntu:~$ nohup ping baidu.com & 1>/dev/null
[1] 3139339
ubuntu@VM-0-9-ubuntu:~$ nohup: ignoring input and appending output to 'nohup.out'
ubuntu@VM-0-9-ubuntu:~$ ps -ef | grep ping
ubuntu 3139339 3128001 0 15:11 pts/2 00:00:00 ping baidu.com
ubuntu 3139503 3128001 0 15:11 pts/2 00:00:00 grep --color=auto ping
ubuntu@VM-0-9-ubuntu:~$ echo $!
3139339
获取上次命令最后一个参数 $_:

这一页的东西不多但都是地基:Shebang 决定谁来解释脚本,bash 和 source 决定变量留在子 shell 还是当前 shell, ~/.profile、/etc/profile 决定变量能活多久,$0 $# $* $@ $? $$ $! 这几个特殊变量写脚本时天天见,先混个脸熟。