首页  /  Linux 进阶  /  正文

Shell 基础认知与变量详解

Linux 进阶 2026-10-02📖 20 分钟👁 —
🐚 Linux 进阶 · Shell 编程第 1 / 10 页12345678910📖 完整导航 →

一、基础认知

为何需要 shell:DevOps 是避不开编程的,以后的运维也是,大势所趋。

1. Linux 底层简单介绍(shell 译为壳)

实现 shell 的程序称为 bash。

需要说明:操作系统是一个整体,内核和 shell 都是它的两个部分。

shell 的作用,以及外围应用程序、shell 解释器、操作系统内核、机器硬件的层次关系

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

命令执行过程:

敲下 ls 之后,命令怎么经 bash 解释器、系统调用走到内核,结果再从终端输出

早期是编写机器语言,编译后运行的,直到解释器的出现。

/usr/bin 下的文件属于二进制文件,打开都是无法阅读的。

2. Shebang

和 shell 的交互可以是非交互式的,代码写到文件,用的时候运行(比如 Linux 的 .sh 文件、win 的 .bat 文件),称为 shell 脚本(非交互式)。

也可以不写后缀,但俗成的东西没必要破坏。

脚本的首行会有 #!(称为 Shebang)和路径,即说明信息,使用 shell 语法编写。

Shebang 的作用是指明解释器去读取,即使脚本不在 /bin 目录,写 Shebang 也可使用 /bin/sh,让指向的解释器执行。

/bin/sh 是一个可执行文件。

以 #!/bin/sh、#!/usr/bin/python、#!/usr/bin/env 开头分别代表什么

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" #注释在这里
network 脚本里的注释:Shebang 指定解释器,开头一段注解说明脚本作用,代码前面再补注释

最好使用英文而非中文(在公司写中文也没问题,只是会涉及系统平台语言不兼容问题)。

执行 shell 脚本的方式

  1. 用 bash/sh 指定文件的绝对路径、相对路径运行(需要 x 执行权限)

白色的就是没有执行权限,绿色才有:

ll 出来的 hello.py 和 hello.sh 都是绿色带 * 的,说明已经有执行权限
  1. source hello.sh、. hello.sh 的方式,具体区别后面了解
  2. 少见:sh < hello.sh
弱类型语言和强类型语言的区别:shell 定义变量不用声明类型

简单来说就是随便定义变量类型不用声明。

选定了学习路径就要坚定,别人说好的不跟风,别人说不好的不迷茫,犹犹豫豫走走停停一定会暴毙。


二、一阶段: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 常用参数

!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:~# 

很早之前就说过变量,把右边赋值给左边,但是本次会深入了解一些变量。

定义变量和赋值之间,不得有空格(加引号是好习惯)。

单引号变量不识别特殊语法,双引号变量能识别特殊符号:

name2 用单引号把 ${name} 原样存进去,name3 用双引号就解析成了实际的值

每次调用 bash/sh 解释器执行脚本都会开启子 shell,因此不保留当前 shell 的变量。

bash make_vars.sh 之后再 echo $name,拿到的还是父 shell 里的旧值:

bash 执行完脚本再 echo $name,值没有变

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

source make_vars.sh 之后再 echo $name,拿到的是脚本里新赋的值

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

name=ls 存进去的是字符串 ls,name=`ls` 才会把 ls 的执行结果存进变量

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

echo $name 出来的文件名没有颜色,直接敲 ls 出来的文件名是带颜色的
一道选择题:脚本里写 user1=`whoami`,用 sh 执行完再 echo $user1,结果是 A 当前用户、B 超哥、C 空

使用 source 命令执行才会输出用户,不产生子 shell。

linux 是 [命令][空格][参数] 操作的对象,无需声明变量类型。

echo $变量名、echo ${变量名} 可以输出,前面是简写。

命名规则

变量命名规则:见名知意、不引用保留关键字、只能数字字母下划线、不能数字开头、不能用标点符号、严格区分大小写

变量作用域

变量作用域:父 shell 里定义的 name、age 到了子 shell 就找不到了,子 shell 可以定义自己的变量
var1=

环境变量,也是全局变量,在所有的 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 的输出:

CentOS 的 ~/.bash_profile:先 source ~/.bashrc,再把 $HOME/.local/bin 和 $HOME/bin 拼进 PATH 并 export

两种系统配置环境变量的方式都是在文件末尾写 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:

两个 ssh 窗口各自的 pstree,ssh 下面的 bash 才是父 shell,用 set | grep son_var 查不到对方窗口的变量

4. Shell 变量加载顺序

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 结束,只读变量失效:

readonly password1=

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

declare -x 打出来的环境变量列表,TERM、USER、XDG_RUNTIME_DIR、XDG_SESSION_ID 这些

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

export 配合 awk 切出来的环境变量名列表:HISTSIZE、HOME、PATH、SHELL、XDG_SESSION_ID 这些

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

分号把 ls /data/、cd /tmp/、cd /home、cd /data 串成一行执行

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

环境变量文件加载顺序:/etc/profile → /etc/profile.d 下的脚本 → $HOME/.bash_profile → $HOME/.bashrc

最后一步是 /etc/bashrc:

加载顺序流程图的最后一格:运行 /etc/bashrc

5. Shell 特殊参数变量

参数变量介绍

特殊变量说明:$0 拿脚本文件名和路径,$n 拿第 n 个参数,$# 拿参数总个数,$* 和 $@ 拿所有参数
special_var.sh 的写法:分别 echo $0 $1 $2、$#、$*、$@,中间用 #### 分隔
跑一遍 special_var.sh yu chao 180 180 180 180,$# 是 6,$* 和 $@ 都是把那几个参数原样列出来

下面是双引号的版本:

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
different.sh 的代码:分别用
跑出来

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

6. Shell 特殊状态变量

状态变量介绍

状态变量:$? 上次命令的返回值、$$ 当前脚本的进程号、$! 上次后台进程的 PID、$_ 上次命令的最后一个参数

图中的 $_ 标错了,表示上一次命令传入的最后一个参数。

获取上次命令退出状态 $?:0 成功,非 0 失败。这里先简单理解,学习函数之后会更好地理解。

脚本执行结束了,返回一个数字 id 作为返回值:

t1.sh 的头几行:$# -ne 2 就打印 must be two args 并 exit 119,否则 echo ok
参数不对时脚本退出码是 119,传够两个参数就是 0

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

脚本末尾加上 echo
连跑 5 次 t1.sh,每次的脚本 id 都在涨:22000 一直涨到 22004

获取上次后台执行程序 PID $!。

nohup xxx & 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

获取上次命令最后一个参数 $_:

echo $_ 打出上一个命令的最后一个参数 180

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