Shell脚本入门:从Hello World到变量的灵活运用

Shell脚本入门:从Hello World到变量的灵活运用

最近团队里有个需求:每天凌晨3点自动备份数据库,备份完成后发一封邮件通知。听起来不复杂对吧?但如果纯手工操作,每天凌晨爬起来敲命令,谁受得了?这时候Shell脚本就派上用场了。今天我们从零开始,聊聊Shell脚本的基础知识。


一、Shell是什么?

简单来说,Shell就是操作系统和用户之间的"翻译官"。你在终端敲的每一条命令,都是Shell帮你解释给操作系统听的。

Linux系统默认的Shell是bash(Bourne Again Shell),你可以通过以下命令确认:

bash 复制代码
echo $SHELL
# 输出:/bin/bash

# 查看系统支持哪些Shell
cat /etc/shells

常见的Shell类型有shbashzsh等,其中bash是最通用的,也是我们编写脚本时最常用的。


二、第一个Shell脚本

2.1 脚本的基本结构

创建一个文件hello.sh

bash 复制代码
#!/bin/bash
# 功能:第一个Shell脚本
# 作者:hengjun
# 日期:2024-01-01

echo "Hello, Shell!"
echo "当前时间是: $(date)"
echo "当前用户是: $(whoami)"

几个要点:

  • 第一行#!/bin/bash叫做Shebang,它告诉系统用哪个解释器来执行这个脚本
  • #开头的行是注释(除了第一行的Shebang)
  • 脚本文件名建议以.sh结尾,方便识别

2.2 执行脚本的几种方式

bash 复制代码
# 方式1:用bash命令执行(推荐,不需要执行权限)
/bin/bash hello.sh

# 方式2:用source执行(在当前Shell环境中执行,会影响当前环境变量)
source hello.sh
# 简写形式
. hello.sh

# 方式3:给执行权限后直接运行
chmod +x hello.sh
./hello.sh

注意: 方式1和方式2的区别很重要。方式1会在一个子Shell中执行,脚本中的变量不会影响当前Shell;方式2会在当前Shell中执行,脚本中定义的变量会保留。在加载环境配置文件时,我们通常用source

2.3 脚本调试

脚本写错了怎么办?bash提供了调试选项:

bash 复制代码
# 检查语法错误(不执行)
bash -n hello.sh

# 显示执行过程(每条命令执行前打印出来)
bash -x hello.sh

bash -x在排查问题时特别有用,它会把每一行命令展开后显示出来,方便你看到变量的实际值。


三、Shell变量:脚本的灵魂

3.1 变量的定义和使用

Shell是弱类型语言,变量不需要声明类型,直接赋值即可:

bash 复制代码
# 定义变量(等号两边不能有空格!这是最常见的错误)
name="zhangsan"
age=25

# 使用变量
echo "姓名: $name, 年龄: $age"
# 或者用花括号明确边界
echo "姓名: ${name}, 年龄: ${age}"

变量命名规范:

  • 只能使用字母、数字和下划线,不能以数字开头
  • 不能使用Shell关键字
  • 建议使用有意义的名称,如user_name而不是un

3.2 三种引号的区别

这是初学者最容易混淆的地方,也是面试常考的点:

bash 复制代码
name="zhangsan"

# 单引号:原样输出,不做任何变量替换
echo 'Hello, $name'        # 输出:Hello, $name

# 双引号:会解析变量
echo "Hello, $name"        # 输出:Hello, zhangsan

# 反引号或$():执行命令并获取输出
echo "当前目录: $(pwd)"     # 输出:当前目录: /root

经验法则: 数字不加引号,字符串默认加双引号,需要原样输出时用单引号。

3.3 命令变量

除了直接赋值,我们还经常需要把命令的执行结果赋给变量:

bash 复制代码
# 方式1:$()(推荐,可嵌套)
current_date=$(date +%F)
file_count=$(ls -la /tmp | wc -l)
ip_addr=$(ifconfig eth0 | grep -w inet | awk '{print $2}')

# 方式2:反引号(功能相同,但不支持嵌套)
current_date=`date +%F`

echo "日期: $current_date"
echo "文件数: $file_count"
echo "IP地址: $ip_addr"

3.4 全局变量与局部变量

bash 复制代码
# 局部变量:只在当前Shell有效
local_var="I am local"

# 全局变量:所有Shell都能访问
export GLOBAL_VAR="I am global"

# 查看所有全局变量
env | grep GLOBAL_VAR

# 删除变量
unset local_var

环境变量文件体系:

复制代码
/etc/profile          # 系统级,所有用户登录时加载
/etc/profile.d/*.sh   # 被/etc/profile调用
~/.bash_profile       # 用户级,用户登录时加载
~/.bashrc             # 用户级,每次打开新终端加载

修改这些文件后,用source命令使其生效:

bash 复制代码
# 比如配置Java环境变量
echo 'export JAVA_HOME=/opt/java' >> /etc/profile.d/java.sh
echo 'export PATH=$JAVA_HOME/bin:$PATH' >> /etc/profile.d/java.sh
source /etc/profile.d/java.sh

四、Shell内置变量

Shell提供了一些特殊的内置变量,在编写脚本时非常有用:

bash 复制代码
#!/bin/bash
# 演示常用内置变量

echo "脚本名称: $0"
echo "第一个参数: $1"
echo "第二个参数: $2"
echo "参数总数: $#"
echo "所有参数: $@"
echo "所有参数(单字符串): $*"
echo "当前进程PID: $$"
echo "上一条命令的返回值: $?"

执行效果:

bash 复制代码
/bin/bash demo.sh arg1 arg2 arg3
# 脚本名称: demo.sh
# 第一个参数: arg1
# 第二个参数: arg2
# 参数总数: 3
# 所有参数: arg1 arg2 arg3
# 当前进程PID: 12345
# 上一条命令的返回值: 0

$?特别重要: 它表示上一条命令的执行状态。0表示成功,非0表示失败。在脚本中做条件判断时经常用到。

bash 复制代码
ls /tmp
echo $?    # 0,成功

ls /not_exist_dir
echo $?    # 2,失败

五、数据格式化输出

5.1 echo命令

echo是最基本的输出命令,但配合一些选项可以实现格式化:

bash 复制代码
# 不换行输出
echo -n "Hello"
echo "World"
# 输出:HelloWorld

# 解析转义字符
echo -e "第一行\n第二行"
# 输出:
# 第一行
# 第二行

# 制表符
echo -e "姓名\t年龄\t城市"
echo -e "张三\t25\t北京"

# 带颜色输出(运维脚本中常用)
echo -e "\033[31m红色文字\033[0m"
echo -e "\033[32m绿色文字\033[0m"
echo -e "\033[33m黄色文字\033[0m"

颜色代码速查:30黑、31红、32绿、33黄、34蓝、35紫、36青、37白。

5.2 printf命令

printfecho更强大,支持C语言风格的格式化输出:

bash 复制代码
# 基本格式化
printf "姓名: %s, 年龄: %d\n" "张三" 25

# 数字补零
printf "编号: %05d\n" 42    # 输出:编号: 00042

# 浮点数精度控制
printf "价格: %.2f元\n" 99.5    # 输出:价格: 99.50元

# 左对齐(负号表示左对齐)
printf "%-10s %-5d %-10s\n" "张三" 25 "北京"
printf "%-10s %-5d %-10s\n" "李四" 30 "上海"

实战:格式化输出系统信息

bash 复制代码
#!/bin/bash
# 系统信息采集脚本

hostname=$(hostname)
ip_addr=$(hostname -I | awk '{print $1}')
os_info=$(cat /etc/redhat-release 2>/dev/null || cat /etc/os-release | grep PRETTY_NAME | cut -d'"' -f2)
mem_total=$(free -m | awk '/Mem/{print $2}')
mem_used=$(free -m | awk '/Mem/{print $3}')

echo -e "\033[32m========== 系统信息 ==========\033[0m"
printf "主机名:   %-20s\n" "$hostname"
printf "IP地址:   %-20s\n" "$ip_addr"
printf "操作系统: %-20s\n" "$os_info"
printf "内存总量: %-20s M\n" "$mem_total"
printf "内存使用: %-20s M\n" "$mem_used"
echo -e "\033[32m=============================\033[0m"

六、数据格式化输入

6.1 重定向

重定向是Shell中非常重要的概念,它控制着命令的输入输出走向:

bash 复制代码
# 标准输出重定向(覆盖)
echo "hello" > file.txt

# 标准输出重定向(追加)
echo "world" >> file.txt

# 标准错误重定向
ls /not_exist 2> error.log

# 标准输出和错误都重定向
command > all.log 2>&1
# 等价于
command &> all.log

# 输入重定向
wc -l < /etc/passwd

6.2 管道符

管道符|把前一个命令的输出作为后一个命令的输入,这是Linux命令组合的核心:

bash 复制代码
# 统计当前登录用户数
who | wc -l

# 查看占用内存最多的5个进程
ps aux | sort -k4 -rn | head -5

# 查看日志中最近的ERROR
tail -100 app.log | grep "ERROR"

6.3 EOF:多行文本输入

在编写脚本时,经常需要生成配置文件。EOF可以方便地实现多行文本的输入:

bash 复制代码
# 生成Nginx配置文件
cat > /etc/nginx/conf.d/app.conf << EOF
server {
    listen 80;
    server_name www.example.com;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host \$host;
    }
}
EOF

注意:如果配置内容中包含$等特殊字符,需要在EOF加引号或者用转义符:

bash 复制代码
# 加引号防止变量替换
cat > config.txt << 'EOF'
HOME=$HOME
USER=$USER
EOF

七、子Shell与环境隔离

理解子Shell的概念对编写复杂脚本很重要:

bash 复制代码
# () 在子Shell中执行,不影响当前环境
(cd /tmp; pwd)    # 切换到/tmp并打印
pwd                # 仍在原目录

# {} 在当前Shell中执行,会影响当前环境
{ cd /tmp; pwd; }  # 切换到/tmp并打印
pwd                # 现在在/tmp了

实际应用场景: 在脚本中临时切换目录做操作,但不想影响后续代码:

bash 复制代码
#!/bin/bash
# 备份脚本

# 用()隔离,备份完成后自动回到原目录
( cd /opt/app && tar -czf /backup/app-$(date +%F).tar.gz . )

# 这里仍在脚本原来的目录
echo "备份完成"

相关推荐
毒爪的小新18 小时前
Linux 环境极速部署 vLLM:从零搭建生产级大模型推理服务
linux·人工智能·ai·语言模型·vllm
鹤落晴春18 小时前
RH124问答3:从命令行管理文件
linux·运维·服务器
凡人叶枫18 小时前
Effective C++ 条款30:透彻了解 inlining 的里里外外
linux·开发语言·c++·嵌入式开发·effective c++
Net_Walke19 小时前
【Linux系统】静态链接库与动态链接库
linux·嵌入式硬件
syc789012319 小时前
中文语境下AI编码工具实战对比:从迭代体验看日常开发选择
linux·人工智能·ubuntu
凡人叶枫19 小时前
Effective C++ 条款22:将成员变量声明为 private
linux·开发语言·c++
vsropy21 小时前
Ubuntu网络图标消失问题/有网络问号
linux·运维·ubuntu
coderwu1 天前
Ubuntu 24.04 终端输入 openclaw config 提示未找到命令解决办法
linux·运维·ubuntu
凡人叶枫1 天前
Effective C++ 条款28:避免使用 handles 指向对象内部
linux·服务器·开发语言·c++·嵌入式开发
AI帮小忙1 天前
Debian系linux操作系统里安装OpenClaw
linux·运维·debian