返回文章列表
帮助中心

Shell 脚本字符串与文件操作全攻略:从基础语法到实战案例

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

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)


本文内容仅供参考,不构成任何专业建议。使用本文提供的信息时,请自行判断并承担相应风险。

分享文章
合作伙伴

本站所有广告均是第三方投放,详情请查询本站用户协议