Linux sh命令

什么是sh命令?

sh(Bourne Shell)是Unix和Linux系统中的经典shell,由Stephen Bourne在1977年开发。它是Unix系统的标准命令解释器,也是POSIX标准shell的基础。

历史背景: sh是最早的Unix shell之一,许多现代shell(如bash、ksh、zsh)都兼容或扩展了sh的功能。在大多数Linux系统中,sh实际上是bashdash的符号链接,运行在POSIX兼容模式下。
注意: 在实际系统中,sh通常是bash(在兼容模式下)或dash(在Debian/Ubuntu系统中)的符号链接。使用ls -l /bin/sh可以查看它指向哪个shell。

语法格式

# 启动交互式shell sh [选项] # 执行脚本文件 sh [选项] 脚本文件 [参数...] # 从字符串执行命令 sh -c "命令字符串" [参数...]

常用选项

选项 描述 示例
-c 从字符串执行命令 sh -c "echo Hello"
-e 遇到错误立即退出 sh -e script.sh
-x 显示执行的命令(调试模式) sh -x script.sh
-v 显示读取的命令(详细模式) sh -v script.sh
-n 检查语法但不执行 sh -n script.sh
-u 使用未定义的变量时报错 sh -u script.sh
-a 将所有变量导出到环境 sh -a script.sh
-- 选项结束符,后面的参数不作为选项 sh -- -file
--version 显示版本信息 sh --version

sh与bash的区别

sh (Bourne Shell/POSIX Shell)
  • 符合POSIX标准,兼容性最好
  • 功能相对简单,语法严格
  • 大多数Unix/Linux系统都可用
  • 启动速度快,占用资源少
  • 适合编写可移植脚本
bash (Bourne Again Shell)
  • 功能丰富,扩展了sh的功能
  • 有命令补全、历史记录等交互功能
  • 支持数组、正则表达式等高级特性
  • 启动较慢,占用资源较多
  • 适合交互式使用和复杂脚本

检查系统上的sh实现:

# 查看sh指向哪个shell ls -l /bin/sh ls -l /usr/bin/sh # 查看sh的版本信息 sh --version # 或 /bin/sh --version # 测试是否是bash sh -c 'echo $BASH_VERSION' # 测试是否是dash sh -c 'echo $0'
# 在Ubuntu/Debian上通常是: # lrwxrwxrwx 1 root root 4 Jan 1 2020 /bin/sh -> dash # 在CentOS/RHEL上通常是: # lrwxrwxrwx. 1 root root 4 Jan 1 2020 /bin/sh -> bash

基本用法

1. 启动交互式sh:

# 启动sh交互式shell sh # 启动sh并执行命令后退出 sh -c "ls -la" # 启动sh并执行多个命令 sh -c "pwd; ls; echo 'Done'" # 从文件执行命令 echo "echo 'Hello from sh'" > commands.txt sh < commands.txt

2. 执行脚本文件:

#!/bin/sh # 这是sh脚本,不是bash脚本 echo "Hello from sh script" # 运行脚本 sh script.sh # 或添加执行权限后直接运行 chmod +x script.sh ./script.sh

3. 调试脚本:

# 检查脚本语法 sh -n script.sh # 调试模式,显示执行的每一行 sh -x script.sh # 详细模式,显示读取的每一行 sh -v script.sh # 组合使用 sh -xv script.sh

变量(sh与bash的差异)

1. 基本变量操作(两者相同):

#!/bin/sh # 变量赋值(等号两边不能有空格) name="Alice" count=10 # 使用变量 echo $name echo "Hello, $name" # 命令替换 current_date=$(date) echo "Today is $current_date"

2. 变量扩展(sh有限制):

#!/bin/sh # 这些在sh中可用 name="Alice" echo ${name} echo "${name}" # 这些在bash中支持,但在sh中可能不支持 # echo ${name^^} # bash: 转大写 # echo ${name,,} # bash: 转小写 # echo ${name:0:3} # bash: 子字符串

3. 特殊变量(两者相同):

#!/bin/sh echo "脚本名: $0" echo "参数个数: $#" echo "所有参数: $@" echo "第一个参数: $1" echo "第二个参数: $2" echo "进程ID: $$" echo "上一个命令退出码: $?"
重要区别: sh不支持bash的很多高级变量扩展功能,如${var^^}${var,,}${var:start:length}等。为了兼容性,应使用POSIX标准语法。

数组(sh的限制)

sh不支持数组(POSIX sh标准不支持数组):

#!/bin/sh # sh中不能使用数组 # 以下代码在sh中会报错 # fruits=("apple" "banana" "orange") # echo ${fruits[0]} # 替代方案:使用位置参数模拟数组 set -- "apple" "banana" "orange" echo "第一个水果: $1" echo "第二个水果: $2" echo "第三个水果: $3" # 或者使用空格分隔的字符串 fruits="apple banana orange" for fruit in $fruits; do echo "水果: $fruit" done

使用位置参数处理"数组":

#!/bin/sh # 模拟数组的函数 array_length() { echo $# } array_get() { shift $1 # 移动参数位置 echo $1 } # 设置"数组"元素 set -- "red" "green" "blue" "yellow" # 获取长度 len=$(array_length "$@") echo "数组长度: $len" # 获取第二个元素 second=$(array_get 1 "$@") echo "第二个元素: $second" # 遍历所有元素 echo "所有元素:" i=1 for item in "$@"; do echo " 元素 $i: $item" i=$((i + 1)) done

算术运算(sh与bash的差异)

1. sh中的算术运算(使用expr或$(( )):

#!/bin/sh # 使用expr命令 a=10 b=3 sum=$(expr $a + $b) echo "和: $sum" diff=$(expr $a - $b) echo "差: $diff" # 注意:expr的*需要转义 product=$(expr $a \* $b) echo "积: $product" quotient=$(expr $a / $b) echo "商: $quotient" # 使用$(( ))(POSIX兼容) sum=$((a + b)) echo "和: $sum" power=$((a ** b)) # 注意:**在POSIX中可能不支持 echo "幂: $power"

2. 浮点运算(使用bc命令):

#!/bin/sh # sh本身不支持浮点运算,需要使用外部命令bc a=10.5 b=3.2 # 使用bc进行浮点运算 sum=$(echo "$a + $b" | bc) echo "和: $sum" # 设置精度 quotient=$(echo "scale=2; $a / $b" | bc) echo "商: $quotient" # 复杂的数学运算 pi=$(echo "scale=10; 4*a(1)" | bc -l) echo "π: $pi"
注意: sh的算术运算功能比bash弱。在sh脚本中,最好使用$(( ))进行整数运算,使用bc进行浮点运算。避免使用bash特有的算术扩展。

条件判断

1. 基本if语句(两者相同):

#!/bin/sh # 基本if语句 if [ condition ]; then commands fi # if-else if [ condition ]; then commands1 else commands2 fi # if-elif-else if [ condition1 ]; then commands1 elif [ condition2 ]; then commands2 else commands3 fi

2. 测试条件(sh使用[ ],bash还可以用[[ ]]):

#!/bin/sh # 字符串比较 [ "$a" = "$b" ] # 等于 [ "$a" != "$b" ] # 不等于 [ -z "$a" ] # 为空 [ -n "$a" ] # 不为空 # 数值比较(注意:sh只能用这些) [ $a -eq $b ] # 等于 [ $a -ne $b ] # 不等于 [ $a -lt $b ] # 小于 [ $a -le $b ] # 小于等于 [ $a -gt $b ] # 大于 [ $a -ge $b ] # 大于等于 # 文件测试 [ -e file ] # 存在 [ -f file ] # 是普通文件 [ -d file ] # 是目录 [ -r file ] # 可读 [ -w file ] # 可写 [ -x file ] # 可执行 [ -s file ] # 非空

3. case语句(两者相同):

#!/bin/sh # case语句 read -p "输入颜色: " color case $color in red) echo "你选择了红色" ;; green|blue) echo "你选择了绿色或蓝色" ;; yellow) echo "你选择了黄色" ;; *) echo "未知颜色" ;; esac # 模式匹配 case "$1" in start|begin) echo "开始" ;; stop|end) echo "结束" ;; *) echo "用法: $0 {start|stop}" ;; esac

循环

1. for循环(sh的for循环有限制):

#!/bin/sh # 遍历列表 for fruit in apple banana orange; do echo "水果: $fruit" done # 遍历参数 for arg in "$@"; do echo "参数: $arg" done # 使用seq命令生成序列 for i in $(seq 1 5); do echo "数字: $i" done # C风格for循环在sh中不支持 # for ((i=0; i<5; i++)); do # 这是bash语法 # echo "i: $i" # done

2. while循环(两者相同):

#!/bin/sh # 基本while循环 count=1 while [ $count -le 5 ]; do echo "计数: $count" count=$((count + 1)) done # 无限循环 while true; do echo "按Ctrl+C退出" sleep 1 done # 读取文件行 while IFS= read -r line; do echo "行: $line" done < file.txt # 从命令读取 ps aux | while read -r user pid cpu mem vsz rss tty stat start time command; do echo "进程 $pid: $command" done

3. until循环(两者相同):

#!/bin/sh # until循环 count=1 until [ $count -gt 5 ]; do echo "计数: $count" count=$((count + 1)) done # 等待条件成立 until ping -c1 -W1 google.com >/dev/null 2>&1; do echo "等待网络连接..." sleep 5 done echo "网络已连接"

函数

1. 函数定义和调用(两者相同):

#!/bin/sh # 函数定义 say_hello() { echo "Hello, $1!" return 0 } # 调用函数 say_hello "Alice" # 获取返回值 say_hello "Bob" echo "返回值: $?" # 0 # 带返回值的函数 add() { sum=$(( $1 + $2 )) echo $sum } result=$(add 10 20) echo "结果: $result"

2. 局部变量(sh不支持local关键字):

#!/bin/sh # sh中没有local关键字,所有变量默认都是全局的 # 可以通过创建子shell来模拟局部变量 myfunc() { # 在子shell中修改变量,不影响父shell ( local_var="我是局部变量" echo "函数内: $local_var" ) # 这里访问不到local_var echo "函数外访问local_var: ${local_var:-未定义}" } # 全局变量 global_var="我是全局变量" myfunc echo "全局变量: $global_var" # 替代方案:使用唯一的变量名 myfunc2() { # 使用函数名前缀避免冲突 myfunc2_var="函数变量" echo "函数变量: $myfunc2_var" }

输入输出

1. 读取输入(sh的限制):

#!/bin/sh # 基本读取 echo -n "请输入姓名: " # -n选项在某些sh中可能不支持 read name echo "你好, $name!" # 读取多值 echo "输入两个数字(空格分隔):" read num1 num2 echo "和: $((num1 + num2))" # sh不支持read的-p、-s等选项 # 替代方案:先echo再read echo "请输入密码: " stty -echo # 关闭回显 read password stty echo # 打开回显 echo echo "密码已接收" # 读取超时(使用外部工具或trap) echo "5秒内输入: " if read -t 5 input 2>/dev/null; then # -t选项在某些sh中不支持 echo "你输入了: $input" else echo "超时!" fi

2. 重定向和管道(两者相同):

#!/bin/sh # 输出重定向 echo "内容" > file.txt # 覆盖 echo "更多内容" >> file.txt # 追加 # 输入重定向 wc -l < file.txt # 错误重定向 command 2> error.log command 2>> error.log command 2>/dev/null # 丢弃错误 # 全部重定向 command > output.log 2>&1 # Here Document cat << EOF 这是一个Here文档 可以包含多行文本 变量替换: $HOME EOF # Here Document(不进行变量替换) cat << 'EOF' 这是一个Here文档 变量不会替换: $HOME EOF

实用脚本示例(POSIX兼容)

1. 简单的文件备份脚本:

#!/bin/sh # 兼容POSIX的备份脚本 set -e # 遇到错误退出 # 配置 BACKUP_DIR="/backup" SOURCE_DIR="$1" DATE=$(date +%Y%m%d_%H%M%S) BACKUP_FILE="$BACKUP_DIR/backup_$DATE.tar.gz" # 检查参数 if [ $# -ne 1 ]; then echo "用法: $0 <源目录>" exit 1 fi # 检查源目录 if [ ! -d "$SOURCE_DIR" ]; then echo "错误: 源目录不存在: $SOURCE_DIR" exit 1 fi # 创建备份目录 mkdir -p "$BACKUP_DIR" # 创建备份 echo "开始备份 $SOURCE_DIR 到 $BACKUP_FILE..." if tar -czf "$BACKUP_FILE" "$SOURCE_DIR"; then # 获取文件大小 if [ -f "$BACKUP_FILE" ]; then size=$(du -h "$BACKUP_FILE" | cut -f1) echo "备份成功: $BACKUP_FILE ($size)" else echo "警告: 备份文件未创建" exit 1 fi else echo "备份失败" exit 1 fi # 删除7天前的备份 echo "清理7天前的备份..." find "$BACKUP_DIR" -name "backup_*.tar.gz" -mtime +7 -delete echo "完成"

2. 系统信息收集脚本:

#!/bin/sh # 系统信息收集脚本(POSIX兼容) # 颜色定义(如果支持) if [ -t 1 ]; then RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color else RED=''; GREEN=''; YELLOW=''; BLUE=''; NC='' fi # 打印带颜色的信息 print_info() { color="$1" label="$2" value="$3" printf "${color}%-20s${NC}: %s\n" "$label" "$value" } echo "=== 系统信息 ===" # 系统信息 print_info "$GREEN" "主机名" "$(uname -n)" print_info "$GREEN" "内核版本" "$(uname -r)" print_info "$GREEN" "系统架构" "$(uname -m)" print_info "$GREEN" "操作系统" "$(uname -s)" # CPU信息 if [ -f /proc/cpuinfo ]; then cpu_model=$(grep "model name" /proc/cpuinfo | head -1 | cut -d: -f2 | sed 's/^[ \t]*//') cpu_cores=$(grep -c "processor" /proc/cpuinfo) print_info "$BLUE" "CPU型号" "$cpu_model" print_info "$BLUE" "CPU核心数" "$cpu_cores" fi # 内存信息 if [ -f /proc/meminfo ]; then total_mem=$(grep "MemTotal" /proc/meminfo | awk '{print $2}') free_mem=$(grep "MemFree" /proc/meminfo | awk '{print $2}') total_mem_mb=$((total_mem / 1024)) free_mem_mb=$((free_mem / 1024)) used_mem_mb=$((total_mem_mb - free_mem_mb)) mem_usage=$((used_mem_mb * 100 / total_mem_mb)) print_info "$YELLOW" "总内存" "${total_mem_mb} MB" print_info "$YELLOW" "已用内存" "${used_mem_mb} MB" print_info "$YELLOW" "内存使用率" "${mem_usage}%" fi # 磁盘信息 echo echo "=== 磁盘使用 ===" df -h | grep -E '^/dev/' | while read line; do fs=$(echo "$line" | awk '{print $1}') size=$(echo "$line" | awk '{print $2}') used=$(echo "$line" | awk '{print $3}') avail=$(echo "$line" | awk '{print $4}') use=$(echo "$line" | awk '{print $5}') mount=$(echo "$line" | awk '{print $6}') printf "%-15s %-8s %-8s %-8s %-8s %s\n" "$fs" "$size" "$used" "$avail" "$use" "$mount" done # 系统负载 echo echo "=== 系统负载 ===" uptime | awk -F'load average:' '{print $2}' | awk '{printf "1分钟: %s\n5分钟: %s\n15分钟: %s\n", $1, $2, $3}' echo echo "信息收集完成于: $(date)"

编写可移植sh脚本的最佳实践

1. Shebang行:

#!/bin/sh # 使用#!/bin/sh而不是#!/bin/bash # 这样可以确保脚本在尽可能多的系统上运行

2. 避免bash特有的特性:

# 不要使用bash特有的语法 # ❌ 不要使用 # [[ ]] # ${var^^} # ${var,,} # ${var:start:length} # arrays=() # local keyword # ✅ 使用POSIX兼容语法 # [ ] # echo "$var" | tr '[:lower:]' '[:upper:]' # expr "$var" : '\(...\)' # set -- "item1" "item2" "item3" # 使用函数名前缀避免变量冲突

3. 使用set命令增强脚本健壮性:

#!/bin/sh # 在脚本开头添加这些选项 set -e # 遇到错误退出 set -u # 使用未定义变量时报错 # set -x # 调试时启用 # 注意:set -o pipefail在POSIX sh中不可用 # 替代方案:检查管道中每个命令的退出状态

4. 变量引用:

# 始终用双引号引用变量,除非有特殊需要 name="John Doe" echo "$name" # ✅ 正确 echo $name # ❌ 可能有问题(包含空格时) # 命令替换也用双引号 files="$(ls)" echo "$files"

5. 测试命令的兼容性:

#!/bin/sh # 测试命令是否存在 command_exists() { command -v "$1" >/dev/null 2>&1 } # 使用前检查命令 if command_exists "grep"; then # 使用grep grep "pattern" file.txt else echo "错误: grep命令不存在" >&2 exit 1 fi # 测试功能 test -n "$(echo .)" || { echo "错误: echo命令异常" >&2 exit 1 }
重要:
1. 不要假设所有系统都有bash
2. 不要使用bash特有的扩展功能
3. 在不同的shell中测试脚本
4. 使用shellcheck检查脚本兼容性
5. 优先使用POSIX标准特性
6. 如果必须使用bash特性,请明确使用#!/bin/bash

测试和验证

1. 使用shellcheck检查脚本:

# 安装shellcheck # Ubuntu/Debian sudo apt-get install shellcheck # CentOS/RHEL sudo yum install shellcheck # 检查脚本 shellcheck script.sh # 指定shell类型 shellcheck -s sh script.sh shellcheck -s bash script.sh

2. 在不同shell中测试:

# 使用dash测试(Ubuntu的默认sh) dash -n script.sh # 检查语法 dash script.sh # 执行脚本 # 使用bash测试(兼容模式) bash --posix -n script.sh bash --posix script.sh # 使用busybox ash测试 busybox sh script.sh

3. 使用checkbashisms检查bashism:

# 安装checkbashisms # Ubuntu/Debian sudo apt-get install devscripts # 检查脚本中的bashism checkbashisms script.sh # 示例输出: # possible bashism in script.sh line 10: # local var="value" # ^-- SC2039: In POSIX sh, 'local' is undefined.

常见问题

主要区别:

1. 功能: bash是sh的增强版,有更多特性
2. 兼容性: sh更兼容各种Unix系统
3. 性能: sh(如dash)通常启动更快
4. 特性: bash支持数组、正则表达式等高级特性

选择建议:
• 编写可移植脚本 → 使用#!/bin/sh
• 需要高级功能 → 使用#!/bin/bash
• 系统初始化脚本 → 使用#!/bin/sh(更快)
• 个人使用脚本 → 使用#!/bin/bash(更方便)

示例:
# 检查当前使用的sh ls -l /bin/sh file /bin/sh

让bash脚本兼容sh的方法:

1. 避免使用bash特有语法:
# ❌ bash特有 array=(1 2 3) local var="value" [[ $var == pattern ]] ${var^^} # ✅ POSIX兼容 set -- 1 2 3 # 不用local,用唯一变量名 [ "$var" = "pattern" ] echo "$var" | tr '[:lower:]' '[:upper:]'
2. 使用bash --posix测试:
bash --posix -n script.sh # 检查语法 bash --posix script.sh # 执行测试
3. 使用工具检查:
shellcheck -s sh script.sh checkbashisms script.sh
4. 替换常见bashism:
# source命令 → .命令 # source file.sh # bash . file.sh # POSIX # 进程替换 # diff <(cmd1) <(cmd2) # bash cmd1 > file1; cmd2 > file2; diff file1 file2 # POSIX

常见原因和解决方法:

1. 系统使用不同的sh实现:
# Ubuntu/Debian使用dash # CentOS/RHEL使用bash # Alpine使用ash # 解决方法:明确指定shell #!/bin/sh # 或 #!/bin/bash
2. 使用了bash特有特性:
# 检查脚本中的bashism checkbashisms script.sh # 使用shellcheck检查 shellcheck script.sh
3. 外部命令路径不同:
# 不要假设命令路径 # ❌ /bin/echo # ✅ 使用env查找 #!/usr/bin/env sh # 或者检查命令是否存在 if command -v grep >/dev/null 2>&1; then # 使用grep else echo "grep not found" >&2 fi
4. 不同的命令选项:
# 某些命令选项在不同系统上不同 # 例如:sed、grep、awk的选项 # 解决方法:使用最通用的选项 # 或检查系统类型 case "$(uname -s)" in Linux*) # Linux特定代码 ;; Darwin*) # macOS特定代码 ;; FreeBSD*) # FreeBSD特定代码 ;; *) # 其他系统 ;; esac

相关命令

命令 描述 与sh的关系
bash Bourne Again Shell,sh的增强版 完全兼容sh,但有更多特性
dash Debian Almquist Shell Ubuntu/Debian的默认sh,轻量快速
ash Almquist Shell dash的前身,BusyBox中的shell
ksh Korn Shell 商业shell,与sh兼容但有扩展
zsh Z Shell 功能丰富的shell,不完全兼容sh
csh C Shell 语法类似C语言,与sh不兼容
shellcheck Shell脚本静态分析工具 用于检查sh/bash脚本问题