sed (Stream Editor, 文本流编辑器) 可谓是 shell 中必不可少一款文本处理工具了。我们经常会使用 sed -i 参数来对一些配置文件做自动化修改,但是此次在 docker 这样做中却遇到了问题
sed: can't move '/etc/redis.confDhPCho' to '/etc/redis.conf': Resource busy
docker-entrypoint.sh 启动部分如下,
# first boot if [ ! -f /data/dump.rdb ]; then if [ -z "$REDIS_PASSWORD" ]; then REDIS_PASSWORD="$(pwgen 16 1)" echo "[INFO] Generated Redis Password: $REDIS_PASSWORD" fi sed -i "s/^# requirepass foobared$/requirepass $REDIS_PASSWORD/" /etc/redis.conf fi
docker-compose.yml 部分配置文件如下,
redis: build: ./redis image: docker-lnmp-redis:latest ports: - "6379:6379" networks: - backend volumes: - ./redis/redis.conf:/etc/redis.conf:rw - ./data/logs/redis/:/var/log/redis/:rw - ./data/redis/:/data/:rw environment: REDIS_PASSWORD: 123456 restart: always container_name: redis
根据启动文件可知由该镜像创建的容器首次启动会对 /etc/redis.conf 文件做修改,而在 docker-compose.yml 中配置外部文件映射到容器内 /etc/redis.conf,也即容器首次启动就会修改这个外部文件。
然而 sed 并不给面子,抛出了一个异常,究其原因,在于 sed -i 此操作看似是修改了文件,但实际上为使用具有修改后的内容的新文件覆盖了原文件,这将导致文件的 Inode 发生变化,而在 docker 中挂载的文件是不允许这样做的。类似的问题还存在于 sed -i 修改符号文件。
$ echo 123 > txt $ stat txt File: txt Size: 4 Blocks: 8 IO Block: 4096 regular file Device: fc01h/64513d Inode: 1179795 Links: 1 Access: (0664/-rw-rw-r--) Uid: ( 1000/ choi) Gid: ( 1000/ choi) Access: 2019-03-09 03:11:32.293005107 +0800 Modify: 2019-03-09 03:11:32.293005107 +0800 Change: 2019-03-09 03:11:32.293005107 +0800 Birth: - # 使用 sed -i 修改文件 Inode 会变 $ sed -i 's/123/abc/' txt $ stat txt File: txt Size: 4 Blocks: 8 IO Block: 4096 regular file Device: fc01h/64513d Inode: 1179862 Links: 1 Access: (0664/-rw-rw-r--) Uid: ( 1000/ choi) Gid: ( 1000/ choi) Access: 2019-03-09 03:12:42.571444788 +0800 Modify: 2019-03-09 03:12:42.571444788 +0800 Change: 2019-03-09 03:12:42.571444788 +0800 Birth: -
为了避免文件覆盖导致 Inode 变化,于是乎有了一个大胆的想法——使用重定向来重写文件内容
$ echo 123 > txt $ stat txt File: txt Size: 4 Blocks: 8 IO Block: 4096 regular file Device: fc01h/64513d Inode: 1179795 Links: 1 Access: (0664/-rw-rw-r--) Uid: ( 1000/ choi) Gid: ( 1000/ choi) Access: 2019-03-09 03:14:10.738505467 +0800 Modify: 2019-03-09 03:14:10.738505467 +0800 Change: 2019-03-09 03:14:10.738505467 +0800 Birth: - # 使用重定向修改不会导致文件 Inode 变化 $ echo abc > txt $ stat txt File: txt Size: 4 Blocks: 8 IO Block: 4096 regular file Device: fc01h/64513d Inode: 1179795 Links: 1 Access: (0664/-rw-rw-r--) Uid: ( 1000/ choi) Gid: ( 1000/ choi) Access: 2019-03-09 03:14:10.738505467 +0800 Modify: 2019-03-09 03:15:19.244883632 +0800 Change: 2019-03-09 03:15:19.244883632 +0800 Birth: -
接着有了如下修改......
sed "s/^# requirepass foobared$/requirepass $REDIS_PASSWORD/" /etc/redis.conf > /etc/redis.conf
然鹅,正当沉浸在找到问题原因所在的欢乐中,现实再一次给了沉痛一击......
是的,Inode 是没变,但是文件内容已经空空如也了!!!
为什么会这样呢,原因在于 I/O 重定向时 stdout 与 stderr 的管道会先准备好再从 stdin 读入,也就是说 > file 时会先将 file 清空然后再读入数据,因此不等到 sed 读数据时文件已经就是空的了。
# I/O 重定向会先清空文件 $ echo 123 > txt $ cat < txt > txt $ cat test.txt
正确做法如下,既保证了 Inode 不变又修改了文件内容
$ echo 123 > txt # 注意不可以再将 tee txt >/dev/null 否则又是空 # https://github.com/koalaman/shellcheck/wiki/SC2094 $ cat txt | sed 's/123/abc/' | tee txt # 或 $ sed 's/123/abc' txt > tmp && cat tmp > txt && rm tmp