Shell Issues

发布时间 2023-09-28 19:12:15作者: Festu

Shell Issues

Debris

  • 双引号包裹下引用变量,其中的换行符号(不是转义)会被解析,否则会被直接忽视(不会被替换为 \n ),例如:

    $pre="
    I
    Love
    You
    "
    echo $pre	# 输出一行内容
    echo "$pre"	# 输出多行内容
    
  • 引用变量时,若其中存储的是字符串且对字符串的转义有严格要求,则形如 "$var" 的变量引用等价于其内容外套一层双引号,该引用过程只发生一次转义。

Solutions

重定向权限与 tee

sudo echo $pre_source > /etc/apt/sources.list

该语句会提示权限不够,因为 >, >> 等重定向符也需要 root 权限。

可以利用 tee 命令进行重定向:

echo "$pre_source" | sudo tee /etc/apt/sources.list

tee 相当于 >tee -a 相当于 >>

Here Document

为"here document"的分隔符加双引号,会让其不解析输入内容中的变量表达式 $<str>

<cmd> << "EOF"
	$abc
EOF

终止符仍为 EOF 不加引号,且必须顶格。


#!/bin/bash
# 设置 root 密码
function root_passwd(){
    passwd -d root > /dev/null
    passwd root <<- EOF > /dev/null
        "$pre_passwd"
        "$pre_passwd"
EOF
    # EOF 好像必须挤在开头, 且后面不能跟别的东西, 包括注释
}

<<- 能让传入的数据忽略制表符 \t

但是在编辑器 VSC 中的 tab 按键产生的缩进实际上是多个空格,而 Vim 下的 tab 产生的缩进用的才是制表符 \t

由于 <<- 只会忽略制表符 \t 而不是空格,因此此处输入的密码前面有数个空格。

同时 hd 下的双引号并不会被解析,因此密码前后各有一个双引号。

shellecho 的转义

shell 真是转义地狱:

#!/bin/bash
pre="
        echo \"
#   for i in {0..255}; do print -Pn \\\"%\\\${i}F\\\${(l:3::0:)i}%f \\\" \\\${\\\${(M)\\\$((i%8)):#7}:+\\\$'\\\\n'}; done
        \"
"

执行 su - festu -c $pre 换行会被转义后输出。

解释:

echo 会对传入的特殊字符转义一次。其转义范围包括 \n, \t 等特殊字符,但不包括 $, `, "

据此来剖析一下上述语句的转义流程。

首先是 $pre 变量被创建,shell 解析双引号括起的字符串时,会对其中的 $, `, " 进行一次转义。因此存入 $pre 中的换行周边的内容为 \\n

接着变量作为参数被给命令 su ,这个过程原样传递。传入的内容又作为 shell 命令被解析,由于换行符仍然是被双引号括起的字符串,因此再次发生转义,\\n 变为 \n 作为参数被传给 echo 命令,然后又被转义,最后变成了换行被输出。

前两次转义对 $, `, " 同样生效,同时又作用于 \\\\n 中的反斜杠,换行符也牵连着被转义了两次(当然这里只是说起了转义的效果,实际上 shell 解析双引号括起的字符串时并不解析换行符本身的转义即 \n

而 echo 的转义只对换行符本身起效,因此换行符最终被转义了三次,区别于 shell 特殊字符的两次。

解决方案:echo 添加 -E 参数取消它自带的特殊字符转义:

pre="
        echo -E \"
#   for i in {0..255}; do print -Pn \\\"%\\\${i}F\\\${(l:3::0:)i}%f \\\" \\\${\\\${(M)\\\$((i%8)):#7}:+\\\$'\\\\n'}; done
        \"
"

值得一提的是 echo 与 shell 的转义非常复杂,反复混用可能会非常混乱,造成"转义地狱"的糟糕情况(虽然 shell 早就是了),而且研究起来价值不高,因此不建议反复混用或深究。


(就深究一点点 = =)

例如 shell 并不解析单引号字符串中的转义,因此理论上下面的脚本也能输出 \n

pre="
        echo '
#   for i in {0..255}; do print -Pn \\\"%\\\${i}F\\\${(l:3::0:)i}%f \\\" \\\${\\\${(M)\\\$((i%8)):#7}:+\\\$'\\\\n'}; done
        '
"

对于 \\\\n ,变量赋值时是双引号字符串,被转义一次,变为 \\n ;执行字符串表示的命令时是单引号字符串,因此不做转义;传给 echo 后被转义一次,变为 \n

理论上该语句只对换行符做了两次转义,最后输出的只是字符 \n 。但实际上输出时仍发生了换行。

除此之外还有很多奇怪的问题...总之不建议深究。


最后作出一个结论,如果是全是使用双引号的需要二次转义的场景(写入配置时),使用 echo -E

重定向

写脚本时看能希望在脚本运行时,永久将『标准错误输入』重定向至某处,利用 exec 可实现:

exec 2>~/errLog		# 重定向至文件
exec 1>/dev/null	# 重定向至空
exec 1>/dev/tty1	# 将标准输出重定向到 tty1 终端

标准输入输出设备标识符:

/proc/self/fd/0:标准输入 0
/proc/self/fd/1:标准输出 1 
/proc/self/fd/2:标准错误输出 2

与另一个用户通信

write bob > /dev/tty2 与同一系统下的另一用户 bob 通信。

su 指定用户执行命令

在交互式 shell 中 su 指定用户执行命令并不能准确读取用户的环境变量如 ~ ,但在脚本中运行时可以。

su - festu -c "
	cd ~;
	mkdir -p Tmp;
	cd Tmp;
	echo $user >> tmp.txt
"
# tmp.txt 中的内容为"festu",有换行可以不要分号
# sudo 执行 Shell 脚本时也能读取正确的用户变量

奇怪的转义特性(感觉是 BUG):

user="festu"
su - festu -c "echo \$user" # 输出空行
su - festu -c "echo $user" # 输出"festu"
su - festu -c "echo \"\$user\"" # 输出"festu"
su - festu -c "echo \"\\\$user\"" # 输出"$user"

当试图用 echo 向配置文件中写入原生的 $HOME 等字符串时,需要注意转义的次数。

Vim 操作与转义脚本

使用场景:

在编写系统配置脚本时,希望用 echo '...' > conf 语句去快速创建配置文件,由于单引号内的字符是绝对的原始字符串,因此无需特殊处理。但如果使用了双引号,就要考虑对原配置文件进行转义处理。

更进一步,脚本中可能需要指定用户执行配置命令,以利用用户的环境变量,如家目录 ~

su - <user> -c "
	echo \"
		...
	\" > ~/conf
"

此时就需要对配置文件的内容进行二次转义。

转义脚本:

#!/bin/bash

if [ -z $1 ];then
	echo "${0} fileName [times]"
	exit 1
fi

if [[ $1 == "-h" || $1 == "--help" ]];then
	echo "Escape special characters in \"Double Quotes\" of shell scripts (for one time or more)."
	exit 1
fi

if [ -z $2 ];then
	times=1
else
	times=$2
fi

for ((i=0;i<${times};i++))
do
	vim $1 <<- EOF
		:%s/\\\\/\\\\\\\\/g
		:%s/\"/\\\\\"/g
		:%s/[\$]/\\\\\$/g
		:%s/\`/\\\\\`/g
		:wq
EOF
done

解析:

非常的美妙不是吗。

首先在终端中键入的(即保存在文本中的) \\ 被 shell 解析时发生第一次转义。

对于表达式 s/<regex>/<string>/g ,首先他会直观地替换 vim 显示的内容(尤其是对于反斜杠)。

表达式中 regex 部分被视为正则表达式,string 部分被视为普通字符串,\\ 由于其特殊性,不论在 regex 部分还是 string 部分都会且仅会被转义一次变为 \

由于替换的"直观性",在 vim 中看到的的所有 \ 会被挨个替换
而对于 $ 字符,由于在正则表达式中它代表结尾,因此在 regex 中表示一个 $ ,需要用 \$

由于这是转义一次后的结果,因此写在脚本中的字符串为 \\$ ;或者用 [$][\\$] 也可以。而在 string 部分 $ 是普通字符,因此不需要转义。

对于反引号 ` ,由于在 shell 脚本中它有特殊含义,因此首先需要用 \\\` 来转义,使其在被 shell 解析后仍为反引号,而不是作为命令解析器被处理。

从这个角度来讲,$ 也应该额外添加反引号做转义,事实上这么做确实是更好的。

回到反引号,在作为 vim 命令模式的参数传入时它和美元符号一样没有特殊含义,因此不再需要额外的转义。