帮助中心
Shell 脚本字符串与文件操作全攻略:从基础语法到实战案例
陶园
2025-12-10
4小时前

Shell 脚本中的字符串操作、文件操作是系统管理和自动化任务的基础技能。通过灵活运用这些技巧,配合 grep、sed、awk 等强大工具,可以高效地完成各种文本处理和文件管理任务。
字符串操作
字符串长度获取
获取字符串长度是最基础的操作:
str="Hello World"echo ${#str} # 输出:11字符串截取
Shell提供了多种字符串截取方式:
str="Hello World"# 从指定位置开始截取到末尾echo ${str:6} # 输出:World# 从指定位置截取指定长度echo ${str:0:5} # 输出:Hello# 从右边截取echo ${str:0-5} # 输出:World字符串替换
替换操作在文本处理中非常常用:
str="Hello World, Hello Shell"# 替换第一个匹配项echo ${str/Hello/Hi} # 输出:Hi World, Hello Shell# 替换所有匹配项echo ${str//Hello/Hi} # 输出:Hi World, Hi Shell# 删除匹配的子串echo ${str//Hello/} # 输出: World, Shell字符串分割
使用 IFS(内部字段分隔符)进行字符串分割:
str="apple,banana,orange"IFS=',' read -ra arr <<< "$str"for item in "${arr[@]}"; do echo "$item"done字符串匹配与删除
filename="script.sh.bak"# 从头删除最短匹配echo ${filename#*.} # 输出:sh.bak# 从头删除最长匹配echo ${filename##*.} # 输出:bak# 从尾删除最短匹配echo ${filename%.*} # 输出:script.sh# 从尾删除最长匹配echo ${filename%%.*} # 输出:script文件操作
文件测试操作符
Shell 提供了丰富的文件测试操作符:
file="/path/to/file"# 常用文件测试[ -e $file ] # 文件是否存在[ -f $file ] # 是否为普通文件[ -d $file ] # 是否为目录[ -r $file ] # 是否可读[ -w $file ] # 是否可写[ -x $file ] # 是否可执行[ -s $file ] # 文件是否非空文件创建与删除
# 创建文件touch newfile.txt# 创建目录mkdir -p /path/to/directory# 删除文件rm file.txt# 删除目录及其内容rm -rf directory/# 安全删除(带确认)rm -i file.txt文件复制与移动
# 复制文件cp source.txt destination.txt# 递归复制目录cp -r source_dir/ dest_dir/# 保留文件属性cp -p source.txt dest.txt# 移动文件或重命名mv oldname.txt newname.txt# 移动到目录mv file.txt /path/to/directory/文件权限管理
更多细节请参考别乱动我的文件!一文搞懂Linux的“门禁系统”——文件权限
# 修改权限(数字模式)chmod 755 script.sh# 修改权限(符号模式)chmod u+x script.shchmod go-w file.txt# 修改所有者chown user:group file.txt# 递归修改chmod -R 644 directory/批量文件操作
# 批量重命名for file in *.txt; do mv "$file" "${file%.txt}.bak"done# 批量删除特定文件find . -name "*.tmp" -type f -delete# 查找并处理文件find . -type f -name "*.log" -exec gzip {} \;文件读取方式
使用 cat 命令读取
基本读取
# 读取整个文件内容cat file.txt# 读取内容并赋值给变量content=$(cat file.txt)echo "$content"# 显示行号cat -n file.txt# 显示非空行的行号cat -b file.txt# 显示制表符和行尾符号cat -A file.txt读取多个文件
# 顺序读取多个文件cat file1.txt file2.txt file3.txt# 合并文件cat file1.txt file2.txt > combined.txt使用 while 循环逐行读取
方法 1:标准方式(推荐)
# 逐行读取文件while IFS= read -r line; do echo "处理: $line"done < file.txt# 说明:# IFS= 防止前导/尾随空格被删除# -r 防止反斜杠转义# < file.txt 重定向文件作为输入方法 2:使用管道(不推荐)
# 注意:在子 shell 中执行,变量不会传递到父 shellcat file.txt | while read line; do echo "$line" count=$((count + 1)) # 这个变量在循环外无法访问done方法 3:保留前后空格
while read -r line; do echo "[$line]" # 会保留空格done < file.txt方法 4:按字段分割读取
# 读取CSV或以特定分隔符分隔的文件while IFS=',' read -r field1 field2 field3; do echo "字段1: $field1" echo "字段2: $field2" echo "字段3: $field3"done < data.csv使用 for 循环读取
# 方式1:读取为单个字符串(不保留换行)for line in $(cat file.txt); do echo "$line"done# 方式2:读取整个文件到数组mapfile -t lines < file.txt# 或readarray -t lines < file.txt# 遍历数组for line in "${lines[@]}"; do echo "$line"done# 访问特定行echo "第一行: ${lines[0]}"echo "第三行: ${lines[2]}"echo "总行数: ${#lines[@]}"使用 read 命令
读取单行
# 读取文件第一行read first_line < file.txtecho "$first_line"# 读取指定行line_number=5sed -n "${line_number}p" file.txt读取多个变量
# 从文件读取多个字段到不同变量read var1 var2 var3 < <(echo "value1 value2 value3")echo "$var1, $var2, $var3"交互式读取用户输入
# 提示用户输入echo -n "请输入你的名字: "read nameecho "你好, $name!"# 读取密码(不显示输入)read -s -p "请输入密码: " passwordechoecho "密码已保存"# 设置超时read -t 5 -p "5秒内输入内容: " input使用 head 和 tail
# 读取前10行head -n 10 file.txt# 读取前5行head -5 file.txt# 读取后10行tail -n 10 file.txt# 从第5行开始读取到末尾tail -n +5 file.txt# 实时监控文件新增内容(常用于日志)tail -f logfile.log# 实时监控并显示行号tail -f logfile.log | nl使用 sed 读取
更多细节请参考Linux 文本三剑客入门:认识 sed
# 读取特定行sed -n '5p' file.txt # 第5行sed -n '1p' file.txt # 第1行sed -n '$p' file.txt # 最后1行# 读取行范围sed -n '5,10p' file.txt # 第5到10行sed -n '5,$p' file.txt # 第5行到末尾# 读取匹配的行sed -n '/pattern/p' file.txt # 包含pattern的行sed -n '/start/,/end/p' file.txt # 从start到end之间的行使用 awk 读取
更多细节请参考Linux 文本三剑客入门:认识 awk
# 读取整个文件awk '{print}' file.txt# 读取特定列awk '{print $1}' file.txt # 第1列awk '{print $1, $3}' file.txt # 第1和第3列# 使用自定义分隔符awk -F':' '{print $1}' /etc/passwd# 条件读取awk '$3 > 100' file.txt # 第3列大于100的行awk '/pattern/ {print $0}' file.txt # 包含pattern的行# 读取特定行awk 'NR==5' file.txt # 第5行awk 'NR>=5 && NR<=10' file.txt # 第5到10行使用 grep 读取
更多细节请参考Linux 文本三剑客入门:认识 grep
# 读取匹配的行grep "pattern" file.txt# 不区分大小写grep -i "pattern" file.txt# 显示行号grep -n "pattern" file.txt# 读取不匹配的行grep -v "pattern" file.txt# 读取匹配的行及上下文grep -A 2 "pattern" file.txt # 匹配行及后2行grep -B 2 "pattern" file.txt # 匹配行及前2行grep -C 2 "pattern" file.txt # 匹配行及前后各2行使用文件描述符读取
# 打开文件描述符3用于读取exec 3< file.txt# 从文件描述符读取while read -u 3 line; do echo "$line"done# 关闭文件描述符exec 3<&-同时读取多个文件
# 并行读取两个文件exec 3< file1.txtexec 4< file2.txtwhile read -u 3 line1 && read -u 4 line2; do echo "文件1: $line1" echo "文件2: $line2" echo "---"doneexec 3<&-exec 4<&-文件写入方式
输出重定向写入可参考本系列第七篇。
使用 printf
# 基本写入printf "Hello World\n" > file.txt# 格式化输出printf "%-10s %-5d %8.2f\n" "Alice" 25 1234.56 > data.txt# 循环写入for i in {1..10}; do printf "%03d: Item %d\n" $i $i >> items.txtdone# 写入制表符分隔的数据printf "%s\t%s\t%s\n" "Name" "Age" "Score" > table.txtprintf "%s\t%d\t%.1f\n" "Alice" 25 95.5 >> table.txtprintf "%s\t%d\t%.1f\n" "Bob" 30 87.3 >> table.txt使用 tee 命令
# 同时输出到终端和文件(覆盖)echo "Log message" | tee log.txt# 追加模式echo "Log message" | tee -a log.txt# 写入多个文件echo "Broadcast" | tee file1.txt file2.txt file3.txt# 在管道中使用ls -l | tee directory.txt | grep ".txt"# 静默写入(不显示在终端)echo "Silent" | tee file.txt > /dev/null# sudo写入受保护的文件echo "new content" | sudo tee /etc/protected.conf > /dev/null使用 sed 写入
# 在文件末尾追加sed -i '$a\New last line' file.txt# 在文件开头插入sed -i '1i\New first line' file.txt# 在特定行后插入sed -i '5a\Inserted after line 5' file.txt# 在匹配行后插入sed -i '/pattern/a\Inserted after pattern' file.txt# 在匹配行前插入sed -i '/pattern/i\Inserted before pattern' file.txt# 替换整行sed -i '3c\This replaces line 3' file.txt# 删除行sed -i '5d' file.txt # 删除第5行sed -i '/pattern/d' file.txt # 删除匹配的行高级读写技巧
大文件处理
逐块读取大文件
# 使用dd按块读取dd if=large_file.bin of=output.bin bs=1M count=10# 分割大文件split -b 100M large_file.txt chunk_# 按行数分割split -l 1000 large_file.txt chunk_流式处理避免内存占用
# 使用管道流式处理cat large_file.txt | while read line; do # 处理每一行,不会一次性加载整个文件 process_line "$line"done# 使用awk流式处理awk '{ # 处理逻辑 print $0}' large_file.txt > output.txt并发读写
并行读取多个文件
#!/bin/bashprocess_file() { local file=$1 echo "处理文件: $file" # 处理逻辑 wc -l "$file"}# 导出函数以便子shell可用export -f process_file# 并行处理find . -name "*.txt" | xargs -P 4 -I {} bash -c 'process_file "{}"'使用后台进程写入不同文件
#!/bin/bash# 启动多个后台写入进程(for i in {1..100}; do echo "File1: $i"; done > file1.txt) &(for i in {1..100}; do echo "File2: $i"; done > file2.txt) &(for i in {1..100}; do echo "File3: $i"; done > file3.txt) &# 等待所有后台进程完成waitecho "所有文件写入完成"原子写入
使用临时文件保证原子性
#!/bin/bashTARGET_FILE="important.conf"TEMP_FILE=$(mktemp)# 写入临时文件cat > "$TEMP_FILE" << EOFcritical_config=valueimportant_setting=trueEOF# 验证内容if grep -q "critical_config" "$TEMP_FILE"; then # 原子性移动(在同一文件系统内是原子操作) mv "$TEMP_FILE" "$TARGET_FILE" echo "写入成功"else rm "$TEMP_FILE" echo "写入失败" >&2 exit 1fi加锁防止并发冲突
使用 flock 文件锁
#!/bin/bashLOCKFILE="/tmp/myapp.lock"DATAFILE="shared_data.txt"# 获取锁并执行( flock -x 200 # 排他锁 # 临界区:读写操作 count=$(cat "$DATAFILE" 2>/dev/null || echo 0) count=$((count + 1)) echo "$count" > "$DATAFILE" echo "当前计数: $count" ) 200>"$LOCKFILE"手动锁机制
#!/bin/bashacquire_lock() { local lockfile=$1 local timeout=10 local elapsed=0 while [ -f "$lockfile" ]; do sleep 1 elapsed=$((elapsed + 1)) if [ $elapsed -ge $timeout ]; then echo "获取锁超时" >&2 return 1 fi done echo $$ > "$lockfile" return 0}release_lock() { local lockfile=$1 rm -f "$lockfile"}# 使用LOCKFILE="/tmp/myapp.lock"if acquire_lock "$LOCKFILE"; then # 执行操作 echo "Data" >> file.txt release_lock "$LOCKFILE"fi二进制文件读写
使用 dd 读写二进制
# 读取二进制文件的前1024字节dd if=binary_file.bin of=header.bin bs=1 count=1024# 跳过前512字节读取dd if=binary_file.bin of=output.bin bs=1 skip=512# 写入二进制数据echo -ne '\x48\x65\x6c\x6c\x6f' > binary.bin# 十六进制查看hexdump -C binary.binxxd binary.bin使用 od 读取二进制
# 八进制格式od binary_file.bin# 十六进制格式od -x binary_file.bin# ASCII格式od -c binary_file.bin实战案例
案例 1:日志文件轮转
#!/bin/bashLOG_FILE="app.log"MAX_SIZE=$((10 * 1024 * 1024)) # 10MBMAX_BACKUPS=5log() { local message="$1" local timestamp=$(date '+%Y-%m-%d %H:%M:%S') # 检查是否需要轮转 if [ -f "$LOG_FILE" ]; then local size=$(stat -f%z "$LOG_FILE" 2>/dev/null || stat -c%s "$LOG_FILE") if [ $size -gt $MAX_SIZE ]; then # 轮转日志 for i in $(seq $((MAX_BACKUPS - 1)) -1 1); do [ -f "${LOG_FILE}.$i" ] && mv "${LOG_FILE}.$i" "${LOG_FILE}.$((i + 1))" done mv "$LOG_FILE" "${LOG_FILE}.1" # 压缩旧日志 [ -f "${LOG_FILE}.$MAX_BACKUPS" ] && gzip "${LOG_FILE}.$MAX_BACKUPS" fi fi # 写入日志 echo "[$timestamp] $message" >> "$LOG_FILE"}# 使用log "应用启动"log "处理请求"log "应用关闭"案例 2:配置文件读写
#!/bin/bashCONFIG_FILE="app.conf"# 读取配置read_config() { local key=$1 local default=$2 if [ -f "$CONFIG_FILE" ]; then local value=$(grep "^${key}=" "$CONFIG_FILE" | cut -d'=' -f2- | tr -d ' ') echo "${value:-$default}" else echo "$default" fi}# 写入配置write_config() { local key=$1 local value=$2 if [ -f "$CONFIG_FILE" ]; then # 更新已存在的配置 if grep -q "^${key}=" "$CONFIG_FILE"; then sed -i.bak "s/^${key}=.*/${key}=${value}/" "$CONFIG_FILE" else # 添加新配置 echo "${key}=${value}" >> "$CONFIG_FILE" fi else # 创建新配置文件 echo "${key}=${value}" > "$CONFIG_FILE" fi}# 删除配置delete_config() { local key=$1 if [ -f "$CONFIG_FILE" ]; then sed -i.bak "/^${key}=/d" "$CONFIG_FILE" fi}# 使用示例write_config "database.host" "localhost"write_config "database.port" "3306"write_config "database.name" "myapp"db_host=$(read_config "database.host" "127.0.0.1")db_port=$(read_config "database.port" "3306")echo "数据库配置: $db_host:$db_port"案例 3:CSV 文件处理
#!/bin/bashCSV_FILE="data.csv"# 读取CSV并处理process_csv() { local line_num=0 while IFS=',' read -r name age city score; do line_num=$((line_num + 1)) # 跳过标题行 [ $line_num -eq 1 ] && continue # 处理数据 echo "处理记录 $line_num:" echo " 姓名: $name" echo " 年龄: $age" echo " 城市: $city" echo " 分数: $score" # 根据条件过滤 if [ "$score" -gt 80 ]; then echo "$name,$age,$city,$score" >> high_scores.csv fi done < "$CSV_FILE"}# 写入CSVwrite_csv() { # 写入标题 echo "Name,Age,City,Score" > "$CSV_FILE" # 写入数据 cat >> "$CSV_FILE" << EOFAlice,25,Beijing,95Bob,30,Shanghai,87Charlie,28,Guangzhou,92David,35,Shenzhen,78EOF}# 使用write_csvprocess_csv案例 4:增量备份脚本
#!/bin/bashSOURCE_DIR="/path/to/source"BACKUP_DIR="/path/to/backup"INDEX_FILE="${BACKUP_DIR}/backup.index"# 读取上次备份的文件列表if [ -f "$INDEX_FILE" ]; then mapfile -t old_files < "$INDEX_FILE"else old_files=()fi# 当前文件列表current_files=()while IFS= read -r -d '' file; do current_files+=("$file")done < <(find "$SOURCE_DIR" -type f -print0)# 比较并备份新增或修改的文件{ for file in "${current_files[@]}"; do rel_path="${file#$SOURCE_DIR/}" # 检查是否需要备份 need_backup=false if [[ ! " ${old_files[@]} " =~ " ${file} " ]]; then # 新文件 need_backup=true elif [ "$file" -nt "$INDEX_FILE" ]; then # 文件被修改 need_backup=true fi if $need_backup; then backup_path="${BACKUP_DIR}/${rel_path}" mkdir -p "$(dirname "$backup_path")" cp -p "$file" "$backup_path" echo "已备份: $rel_path" fi # 写入新索引 echo "$file" done} > "${INDEX_FILE}.new"mv "${INDEX_FILE}.new" "$INDEX_FILE"echo "备份完成"案例 5:多进程日志聚合
#!/bin/bashLOG_DIR="logs"AGGREGATED_LOG="aggregated.log"TEMP_DIR=$(mktemp -d)# 并行读取多个日志文件aggregate_logs() { local files=($LOG_DIR/*.log) local pids=() for file in "${files[@]}"; do ( # 为每个文件添加来源标识 while read -r line; do echo "[$(basename $file)] $line" done < "$file" > "$TEMP_DIR/$(basename $file)" ) & pids+=($!) done # 等待所有进程完成 for pid in "${pids[@]}"; do wait $pid done # 合并所有处理后的日志并按时间排序 cat "$TEMP_DIR"/*.log | sort > "$AGGREGATED_LOG" # 清理 rm -rf "$TEMP_DIR"}aggregate_logsecho "日志聚合完成: $AGGREGATED_LOG"性能优化建议
避免频繁的小文件操作
# ❌ 低效:每次循环都打开关闭文件for i in {1..10000}; do echo "Line $i" >> file.txtdone# ✅ 高效:一次性写入{ for i in {1..10000}; do echo "Line $i" done} > file.txt# ✅ 或使用printfprintf "Line %d\n" {1..10000} > file.txt批量操作代替逐行处理
# ❌ 低效:逐行使用 sedwhile read line; do echo "$line" | sed 's/old/new/'done < file.txt# ✅ 高效:一次性处理sed 's/old/new/' file.txt使用合适的工具
# ❌ 低效:用循环统计count=0while read line; do count=$((count + 1))done < file.txtecho $count# ✅ 高效:用wc命令wc -l < file.txt安全注意事项
防止路径注入
# 验证文件路径validate_path() { local path=$1 local base_dir="/safe/directory" # 解析真实路径 real_path=$(realpath "$path" 2>/dev/null) # 检查是否在允许的目录内 if [[ "$real_path" != "$base_dir"* ]]; then echo "非法路径" >&2 return 1 fi return 0}权限控制
# 创建文件时设置安全权限(umask 077; echo "sensitive data" > secure.txt)# 检查文件权限check_permissions() { local file=$1 local perms=$(stat -c %a "$file" 2>/dev/null) if [ "$perms" != "600" ]; then echo "警告: 文件权限不安全" >&2 chmod 600 "$file" fi}临时文件安全
# 使用 mktemp 创建安全的临时文件TEMP_FILE=$(mktemp)trap "rm -f $TEMP_FILE" EXIT# 写入临时文件echo "data" > "$TEMP_FILE"# 处理...# 脚本退出时自动清理(通过trap)本文内容仅供参考,不构成任何专业建议。使用本文提供的信息时,请自行判断并承担相应风险。
分享文章



