# 【shell/bash 提问】如何修改文件内容？

**URL:** <https://meta.appinn.net/t/topic/59057>\
**Category:** 问题求助\
**Tags:** linux\
**Created:** [2024 年7 月 21 日 13:46 UTC](https://meta.appinn.net/t/topic/59057 "2024-07-21T13:46:03Z")\
**Posts on this page:** 13\
**Page:** 1

<div class="post-metadata">

**Author:** ![tempUserName](https://meta-edge.appinn.com/user_avatar/meta.appinn.net/tempusername/32/57781_2.png) [@tempUserName](https://meta.appinn.net/u/tempUserName)\
**Post date:** [2024 年7 月 21 日 13:46 UTC](https://meta.appinn.net/t/topic/59057/1 "2024-07-21T13:46:03Z")

</div>

平常都用高级语言，bash 基本不会。

很简单的问题：

1. 有三个变量 $oldContent，$newContent，$filePath
2. 需求：将文件 $filePath 的内容中的 $oldContent 改为 $newContent

注意点：

1. 只可修改一次，不可 global replace
2. $oldContent，$newContent 的值是 HTML string，注意转义
3. 如果不存在 $oldContent ，报错退出

> 第二个问题，bash、vbs、bat、ps1 中的 `变量` 好像不是高级语言里的 `变量`，更像是 C 里的宏定义？

---

<div class="post-metadata">

**Author:** ![CCR](https://meta-edge.appinn.com/user_avatar/meta.appinn.net/ccr/32/68442_2.png) [@CCR](https://meta.appinn.net/u/CCR)\
**Post date:** [2024 年7 月 21 日 14:27 UTC](https://meta.appinn.net/t/topic/59057/2 "2024-07-21T14:27:41Z")

</div>

弱弱问一句，文本量很大，且必须用命令行吗，为何不用python呢……

我的话会直接招出来gedit……，然后文本自己转义（

* * *

另，可以看看pwsh，全平台通用，不过文本量大的话我还是推荐python

另附pwsh的部分你需要的函数

[https://www.delftstack.com/zh/howto/powershell/replacing-strings-in-powershell](https://www.delftstack.com/zh/howto/powershell/replacing-strings-in-powershell)

> **[在 Windows PowerShell 中逐行读取文件](https://www.delftstack.com/zh/howto/powershell/read-files-line-by-line-in-windows-powershell/)**
>
> 本文将向你展示如何在 Windows PowerShell 中逐行读取文件。这将避免在读取非常大的文件时脚本环境崩溃和冻结。

---

<div class="post-metadata">

**Author:** ![tiger](https://meta-edge.appinn.com/user_avatar/meta.appinn.net/tiger/32/23490_2.png) [@tiger](https://meta.appinn.net/u/tiger)\
**Post date:** [2024 年7 月 21 日 14:31 UTC](https://meta.appinn.net/t/topic/59057/3 "2024-07-21T14:31:42Z")

</div>

您好，本次尝试使用豆包解决问题，分享链接为

> **[和 豆包 的对话 - 豆包](https://www.doubao.com/thread/w442224b4dec3f17b)**
>
> 以下是一个 Bash 脚本示例，可以实现您描述的需求：

提示词是这样构成的：

> 在bash脚本中，有三个变量 $oldContent，$newContent，$filePath  
> 需要处理的问题是：将文件 $filePath 的内容中的 $oldContent 改为 $newContent  
> 但是要注意点：  
> 只可修改一次，不可 global replace  
> $oldContent，$newContent 的值是 HTML string，注意转义  
> 如果不存在 $oldContent ，报错退出

取得的回答中，代码部分是：

```auto
#!/bin/bash

oldContent="$1"
newContent="$2"
filePath="$3"

if [! -f "$filePath"]; then
    echo "文件 $filePath 不存在"
    exit 1
fi

content=$(< "$filePath")

if! echo "$content" | grep -q "$oldContent"; then
    echo "未找到 $oldContent ，报错退出"
    exit 1
fi

newContentEscaped=$(sed '/[\/&]/\\&/g' <<< "$newContent")
oldContentEscaped=$(sed '/[\/&]/\\&/g' <<< "$oldContent")

sed -i "0,/${oldContentEscaped}/s//${newContentEscaped}/" "$filePath"

```

---

<div class="post-metadata">

**Author:** ![tempUserName](https://meta-edge.appinn.com/user_avatar/meta.appinn.net/tempusername/32/57781_2.png) [@tempUserName](https://meta.appinn.net/u/tempUserName)\
**Post date:** [2024 年7 月 21 日 14:55 UTC](https://meta.appinn.net/t/topic/59057/4 "2024-07-21T14:55:51Z")

</div>

原因很简单，这段代码不是给我用的，是给用户用的。

我的下面这个项目，安装插件需要修改入口文件。因为是入口，所以没有任何环境可以用。

我一直以来的做法是写 go 代码，然后编译。问题就是整个项目 10M+，这个文件占了 6M。

我现在想改成 `install_windows.ps1`，`install_linux.sh`。只用简单的 shell 实现。

> **[GitHub - obgnail/typora\_plugin: Typora plugin. Feature enhancement tool | Typora...](https://github.com/obgnail/typora_plugin)**
>
> Typora plugin. Feature enhancement tool | Typora 插件，功能增强工具

---

<div class="post-metadata">

**Author:** ![VictoryRoad](https://meta-edge.appinn.com/user_avatar/meta.appinn.net/victoryroad/32/58964_2.png) [@VictoryRoad](https://meta.appinn.net/u/VictoryRoad)\
**Post date:** [2024 年7 月 21 日 15:01 UTC](https://meta.appinn.net/t/topic/59057/5 "2024-07-21T15:01:04Z")

</div>

调用 grep 和 sed 可以实现：

```auto
#!/bin/sh

FILEPATH="/usr/share/icons/"
OLD="usr"
NEW="bin"

echo $FILEPATH | grep $OLD #判断FILEPATH中是否包含OLD字符串，如果不包含，会返回1
if [$? -eq 1]
then
    echo "ERROR!"
    return 1
else
    TEMP=`echo $FILEPATH | sed "s/$OLD/$NEW/g"`
    FILEPATH=`echo $TEMP`
fi

```

---

<div class="post-metadata">

**Author:** ![tempUserName](https://meta-edge.appinn.com/user_avatar/meta.appinn.net/tempusername/32/57781_2.png) [@tempUserName](https://meta.appinn.net/u/tempUserName)\
**Post date:** [2024 年7 月 21 日 15:05 UTC](https://meta.appinn.net/t/topic/59057/6 "2024-07-21T15:05:51Z")

</div>

你好，感谢帮助。

代码不太行，原因是当 OLD、NEW 包含 斜杠、尖括号、反斜杠等等需要转义的内容是 `sed "s/$OLD/$NEW/g"` 会水土不服。

---

<div class="post-metadata">

**Author:** ![RachelSherman](https://meta-edge.appinn.com/letter_avatar_proxy/v4/letter/r/74df32/32.png) [@RachelSherman](https://meta.appinn.net/u/RachelSherman)\
**Post date:** [2024 年7 月 21 日 15:10 UTC](https://meta.appinn.net/t/topic/59057/7 "2024-07-21T15:10:56Z")

</div>

Unix下可以考虑用sed实现，诸如 `sed -i 's/old/new/' filepath`，Windows下面用PowerShell可以用一些现成的dotNet的库，用 Get-Content 、-replace [regex]::Escape($oldContent), $newContent, 1、Set-Content等等组合一下可以做。

但要注意一个事情：如果文件很大，那么性能可能会很差，对sed这样的流处理的可能还好一点，pwsh下可能就不容乐观了。

至于转义，这个需要专门处理，没有命令行工具会主动帮你处理转义问题。当然还有一个奇技淫巧，那就是在脚本中先把文件里面需要转义的所有字符替换成特殊的不需要转义的字符，进行操作之后再替换回来，当然这个需要特定的条件，不具备普遍适用性。

---

<div class="post-metadata">

**Author:** ![tempUserName](https://meta-edge.appinn.com/user_avatar/meta.appinn.net/tempusername/32/57781_2.png) [@tempUserName](https://meta.appinn.net/u/tempUserName)\
**Post date:** [2024 年7 月 21 日 15:46 UTC](https://meta.appinn.net/t/topic/59057/8 "2024-07-21T15:46:30Z")

</div>

感谢回答，其实我一开始的问题就是不知道 sed 参数该怎么转义。

我不想硬磕这个玩意，来论坛问问有没有其他好方法。既然没有，只能硬着头皮上了 😔

---

<div class="post-metadata">

**Author:** ![tempUserName](https://meta-edge.appinn.com/user_avatar/meta.appinn.net/tempusername/32/57781_2.png) [@tempUserName](https://meta.appinn.net/u/tempUserName)\
**Post date:** [2024 年7 月 21 日 16:20 UTC](https://meta.appinn.net/t/topic/59057/9 "2024-07-21T16:20:10Z")

</div>

网络上各种查，没查到，倒是看到了一句 `escaping in bash is a nightmare` :rofl:

---

<div class="post-metadata">

**Author:** ![3\_5105](https://meta-edge.appinn.com/user_avatar/meta.appinn.net/3_5105/32/57748_2.png) [@3\_5105](https://meta.appinn.net/u/3_5105)\
**Post date:** [2024 年7 月 21 日 16:43 UTC](https://meta.appinn.net/t/topic/59057/10 "2024-07-21T16:43:56Z")

</div>

go语言编译文件过大的话，考虑一下C或者C++？这种标准库应该完全够用  
另外，此处是我给出的批处理示例，读取x.log的每一行，替换LABE字样为5105字样

```auto
@echo off
setlocal enabledelayedexpansion
set "filePath=x.log"
set "oldContent=LABE"
set "newContent=5105"

if exist "new_%filePath%" del /f/q "new_%filePath%"
set r=0
for /f "delims=" %%i in (%filePath%) do (
    set v=%%i
    echo !v:%oldContent%=%newContent%!>>new_%filePath%
    if not "!v:%oldContent%=%newContent%!"=="!v!" set r=1
)
if %r%==0 echo 未找到需替换的字符串！
pause

```

---

<div class="post-metadata">

**Author:** ![Sonar](https://meta-edge.appinn.com/letter_avatar_proxy/v4/letter/s/57b2e6/32.png) [@Sonar](https://meta.appinn.net/u/Sonar)\
**Post date:** [2024 年7 月 21 日 16:59 UTC](https://meta.appinn.net/t/topic/59057/11 "2024-07-21T16:59:15Z")

</div>

借助 od 命令，将 `$oldContent`、`$newContent`、`$filePath` 转换为八进制后进行替换与对比，再使用 printf 命令将八进制转换为文本。

```auto
#!/bin/sh

filePath=$1
oldContent=$2
newContent=$3

oldContentOctal=$( printf '%s' "$oldContent" | od -An -to1 | tr -d '\n' )
newContentOctal=$( printf '%s' "$newContent" | od -An -to1 | tr -d '\n' )

oldFileOctal=$( od -An -to1 "$filePath" | tr -d '\n' )
newFileOctal=$( printf '%s' "$oldFileOctal" | sed "s/$oldContentOctal/$newContentOctal/" )

["$oldFileOctal" = "$newFileOctal"] && exit 2

printf "$( printf '%s' "$newFileOctal" | tr ' ' '\\' )"

```

存在替换，退出码 `$?` 为 0  
不存在替换，退出码 `$?` 为 2

---

<div class="post-metadata">

**Author:** ![tempUserName](https://meta-edge.appinn.com/user_avatar/meta.appinn.net/tempusername/32/57781_2.png) [@tempUserName](https://meta.appinn.net/u/tempUserName)\
**Post date:** [2024 年7 月 28 日 13:06 UTC](https://meta.appinn.net/t/topic/59057/12 "2024-07-28T13:06:54Z")

</div>

我采用比较简单的方法 escape 了。这个方法需要将整个文件读进去。不过因为此文件也就几十 K，也就无所谓了。

```bash
escape() {
    sed -E 's/[]\/$*.^|[]/\\&/g' <<<"$1"
}

escapedOldContent=$(escape "$oldContent")
escapedNewContent=$(escape "$newContent")

newContent=$(echo "$fileContent" | sed "s|$escapedOldContent|$escapedNewContent|")

```

---

<div class="post-metadata">

**Author:** ![Sonar](https://meta-edge.appinn.com/letter_avatar_proxy/v4/letter/s/57b2e6/32.png) [@Sonar](https://meta.appinn.net/u/Sonar)\
**Post date:** [2024 年7 月 28 日 15:15 UTC](https://meta.appinn.net/t/topic/59057/13 "2024-07-28T15:15:34Z")

</div>

提个建议哈，可以用 `printf '%s' "$fileContent"` 或者 `echo -n "$fileContent"`，用 `echo "$fileContent"` 的话会在末尾加上一个额外的换行符
