问题阐述
在学习Bash的过程中,发现存在输出乱码“????”的情况,这里先说结论,排除的结果为“Mac终端echo命令在shell文件中不支持中文和变量之间无空格”。说来可能比较绕口,简单的可以理解为mac不支持中文和变量混合输出。
具体的排查原因写在后面,下面是本次输出乱码的代码:
# !/bin/bash
if [ -z "$1" ]; then
arg1="1"
else
arg1="$1"
fi
if [ -z "$2" ]; then
arg2="2"
else
arg2="$2"
fi
if [[ $arg1 -eq $arg2 ]]; then
echo "$arg1等于$arg2"
elif [[ $arg1 -gt $arg2 ]]; then
echo "$arg1大于$arg2"
elif [[ $arg1 -lt $arg2 ]]; then
echo "$arg1小于$arg2"
else
echo "$arg1不等于$arg2"
fi
问题就是在shell文件中,执行上述代码时,会输出:
????
配置环境
- 客户端:macOS Monterey 12.7.3
- 客户端:macOS Sonoma 14.4.1
- 虚拟机:CentOS Linux release 7.9.2009 (Core)(有可视化界面)
- 服务器:CentOS Linux release 7.9.2009 (Core)(无可视化界面)
本次问题发生在客户端1(macOS Monterey 12.7.3)上,问题出在mac系统,如果Linux等系统存在类似的问题,该教程未必适合。
排查过程
1、排查代码和配置环境
首先,在执行输出乱码代码时,发现输出为????,接着修改代码进行排查,给代码添加调试后,代码输出如下:
# !/bin/bash
set -x
if [ -z "$1" ]; then
...
else
echo "$arg1和$arg2不相等"
fi
set +x
输出的结果为:
% ./1.sh 1 3
+ '[' -z 1 ']'
+ arg1=1
+ '[' -z 3 ']'
+ arg2=3
+ [[ 1 = 3 ]]
+ [[ 1 > 3 ]]
+ [[ 1 < 3 ]]
+ echo $'?\224?\217'
????
+ set +x
根据添加的调试代码可以看出,前面的赋值、判定都是好的,进入正常,但是到 echo 这步就变成了 “????”。
接着开始怀疑是不是环境配置的问题。
1)通过执行file命令来检查文件编码
file 1.sh
输出的结果为:
1.sh: Bourne-Again shell script text executable, Unicode text, UTF-8 text
输出的内容表示脚本文件为UTF-8编码。
2)接着设置终端字符集为UTF-8:
export LC_ALL=en_US.UTF-8
输出的结果为:
% export LC_ALL=en_US.UTF-8
% ./1.sh
????
3)设置终端语言环境为支持中文的环境:
% export LANG=zh_CN.UTF-8
% ./1.sh
????
问题仍然存在。
4)然后在脚本中添加终端字符集:
#!/bin/bash
export LANG=zh_CN.UTF-8
if [ -z "$1" ]; then
...
echo "$arg1和$arg2不相等"
fi
输出的结果为:
% ./1.sh
????
问题仍然存在。
5)Unicode 的转义序列来输出中文字符
下面是完整的使用Unicode转移序列输出中文字符的代码:
#!/bin/bash
export LANG=zh_CN.UTF-8
if [ -z "$1" ]; then
arg1="1"
else
arg1="$1"
fi
if [ -z "$2" ]; then
arg2="2"
else
arg2="$2"
fi
if [[ $arg1 -eq $arg2 ]]; then
echo -e "$arg1\xe7\xad\x89\xe4\xba\x8e$arg2"
elif [[ $arg1 -gt $arg2 ]]; then
echo -e "$arg1\xxe6\xaf\x94$arg2\xxe5\xa4\xa7"
elif [[ $arg1 -lt $arg2 ]]; then
echo -e "$arg1\xxe6\xaf\x94$arg2\xxe5\xb0\x8f"
else
echo -e "$arg1\xxe5\x92\x8c$arg2\xxe4\xb8\x8d\xe7\x9b\xb8\xe7\xad\x89"
fi
输出内容:
% ./1.sh
1\xxe6??2\xxe5??
使用 \x 形式的Unicode转义序列并没有解释成对应的中文字符。
5)使用echo -e进行输出
#!/bin/bash
export LANG=zh_CN.UTF-8
...
if [[ $arg1 -eq $arg2 ]]; then
echo -e "$arg1等于$arg2"
elif [[ $arg1 -gt $arg2 ]]; then
echo -e "$arg1比$arg2大"
elif [[ $arg1 -lt $arg2 ]]; then
echo -e "$arg1比$arg2小"
else
echo -e "$arg1和$arg2不相等"
fi
输出的结果为:
% ./1.sh
????
这里,即使尝试使用Unicode字符直接来替代转义序列,用 echo -e 输出,得出的结果仍然是乱码,然后我们开始尝试printf命令。
7)使用 printf 命令输出
#!/bin/bash
export LANG=zh_CN.UTF-8
if [ -z "$1" ]; then
arg1="1"
else
arg1="$1"
fi
if [ -z "$2" ]; then
arg2="2"
else
arg2="$2"
fi
if [[ $arg1 -eq $arg2 ]]; then
printf "%s等于%s\n" "$arg1" "$arg2"
elif [[ $arg1 -gt $arg2 ]]; then
printf "%s比%s大\n" "$arg1" "$arg2"
elif [[ $arg1 -lt $arg2 ]]; then
printf "%s比%s小\n" "$arg1" "$arg2"
else
printf "%s和%s不相等\n" "$arg1" "$arg2"
fi
输出的代码为:
% ./1.sh
1比2小
这里终于输出成功了,得到了我们理想的答案。但是为什么printf可以输出中文,但是echo就输出乱码。
我从网络上搜索得到的结论为:
在不同的操作系统和终端环境中,echo 命令可能对特殊字符(如中文字符)的处理方式略有不同。一些版本的 echo 命令可能不支持直接输出 Unicode 字符,或者对于特殊字符的处理方式可能与 printf 不同。
通常来说,printf 命令更加灵活,可以通过格式化控制符来精确地控制输出的格式和内容,包括对特殊字符的处理。因此,使用 printf 命令来输出中文字符通常会更加可靠和灵活。
另外,一些系统中的 echo 命令可能支持 -e 选项来启用转义序列,从而支持输出特殊字符,但并不是所有系统都支持这个选项。因此,对于跨平台兼容性或者特殊字符的输出,printf 命令是更好的选择。
问题虽然解决了,但是问题的原因没有搞清楚,因此 ,我们接下来通过升级shell来排查问题是否跟版本有关。
2、升级bash和zsh版本
然后,我开始考虑是不是Shell版本导致的问题,我把代码完整的赋值到虚拟机和服务器上执行,注意他们都是CentOS系统。
1)虚拟机输出正常,Shell版本为:bash 4.2.46
2)服务器输出正常,Shell版本为:zsh 5.0.2
3)本机(macOS Monterey 12.7.3)输出乱码,Shell版本为:zsh 5.8.1
根据上面三张图片可以看到,本机的zsh版本比较高,但是输出乱码。两台CentOS机器输出正常,我在想是不是mac需要更新一下最新的版本才可以输出正常,下面是升级bash和zsh的教程:
1)安装新的 Bash 版本:
brew install bash
安装完成后,将新的Bash添加到可用的Shell列表:
sudo bash -c 'echo /usr/local/bin/bash >> /etc/shells'
使用chsh命令更改默认Shell:
chsh -s /usr/local/bin/bash
2)安装新的 Zsh 版本:
brew install zsh
安装完成后,将新的Zsh添加到可用的Shell列表:
sudo bash -c 'echo /usr/local/bin/zsh >> /etc/shells'
使用chsh命令更改默认Shell:
chsh -s /usr/local/bin/zsh
因为,我是用的是Zsh,所以,我只需要安装最新的Zsh版本即可。
注意:安装完成后,需要重新启动终端:关闭并重新打开终端,或者注销并重新登录系统,以确保新的 Zsh 已经生效。
最后使用bash –version 或者 zsh –version 命令来验证安装的Shell版本。
下面是我安装Zsh的截图
安装成功后,$SHELL –-version输出的是 zsh 5.8.1,但是直接执行 /usr/local/bin/zsh –verison的输出是 zsh 5.9。
3)因为执行 /usr/local/bin/zsh –version 显示的是新安装的Zsh版本号,因此我将其设置为默认Shell:
sudo chsh -s /usr/local/bin/zsh
但实际执行chsh命令后,
% sudo chsh -s /usr/local/bin/zsh
Password:
Changing shell for root.
% $SHELL --version
zsh 5.8.1 (x86_64-apple-darwin21.0)
实际的zsh版本号并没有改变,后面我重新打开新的会话,也没有改变zsh版本号。
这里可能是权限或其他问题导致的,我然后尝试手动修改用户的默认Shell,而不使用 sudo:
% chsh -s /usr/local/bin/zsh
Changing shell for fangjunyu.
Password for fangjunyu:
chsh: /usr/local/bin/zsh: non-standard shell
执行后,系统提示/usr/local/bin/zsh 是非标准的 Shell。
因此需要将Zsh添加到标准Shell列表:
sudo bash -c 'echo /usr/local/bin/zsh >> /etc/shells'
并再次尝试切换默认Shell:
chsh -s /usr/local/bin/zsh
实际操作如下:
% sudo bash -c 'echo /usr/local/bin/zsh >> /etc/shells'
% chsh -s /usr/local/bin/zsh
Changing shell for fangjunyu.
Password for fangjunyu:
% $SHELL --version
zsh 5.8.1 (x86_64-apple-darwin21.0)
问题仍然未解决。
4)然后我尝试使用dscl命令进行切换默认Shell:
首先,通过用户名查找唯一标识符(UID)
id -u fangjunyu
501
然后使用 dscl 命令修改默认Shell
sudo dscl . -change /Users/fangjunyu UserShell /bin/zsh /usr/local/bin/zsh
fangjunyu为我的登录用户和用户空间,具体修改根据自身的情况而定。
我在执行这一步时,出现了下面的这个报错:
% sudo dscl . -change /Users/fangjunyu UserShell /bin/zsh /usr/local/bin/zsh
<main> attribute status: eDSAttributeNotFound
<dscl_cmd> DS Error: -14134 (eDSAttributeNotFound)
问题为在用户记录中找不到UserShell属性,这表示用户记录中没有默认Shell的字段。
因此,我需要使用dscl命令直接设置默认Shell,而不是修改现有的字段:
sudo dscl . -create /Users/fangjunyu UserShell /usr/local/bin/zsh
执行上述命令后,会创建一个新的 UserShell 属性,并将其设置为 /usr/local/bin/zsh。
我再次打开新的终端会话时,Shell版本号变成了最新的zsh 5.9,但是执行1.sh仍然报????,这也表示无法通过升级Shell版本来解决这一问题。
3、定位代码问题
经过一系列排查和升级后,问题仍未解决,我开始思考,是不是还是代码的问题?我尝试将代码的echo输出内容改为:
echo "$arg1$arg2"
当去除中文时,变量是正常输出的,而中文和变量放在一起时,就出毛病,输出乱码。
我又找到一个解决方法,就是在输出中加入引号:
#!/bin/bash
set-x
...
if [[ $arg1 -eq $arg2 ]]; then
echo "\"$arg1\"等于\"$arg2\""
elif [[ $arg1 -gt $arg2 ]]; then
echo "\"$arg1\"比\"$arg2\"大"
elif [[ $arg1 -lt $arg2 ]]; then
echo "\"$arg1\"比\"$arg2\"小"
else
echo "\"$arg1\"和\"$arg2\"不相等"
fi
set+x
这次输出是正常的。
但实际上跟前面的printf是一样的,治标不治本,有解决方案,但是不知道实际的问题根源。导致是什么问题造成的?还需要继续排查。
4、更换终端字体、配置和程序
经过上述排查后,我现在开始考虑是不是评估终端不支持特定字符或者配置出了问题导致的乱码。
接着,我开始尝试在终端中使用支持中文的字体,如“Menlo”、“Monaco”、“Courier”等字体,并将终端的语言环境变量改为UTF-8,并且尝试使用其他的终端程序,如iTerm2。
1)设置终端字体
2)设置终端的文本编码
3)iTerm2程序
经过终端字体、配置和程序的更换,问题始终没有得到解决,甚至出现了新的乱码形式:
% ./1.sh
����
既然问题都不行,我想这个问题也就到这了。
在想要放弃的时候,想到还有一台mac(macOS Sonoma 14.4.1),如果另一台mac也不行,大概率就是mac不支持这种代码,具体是什么代码,可能就是中文和变量混在一起的形式,是不被mac所支持的。
答案也是这样的结论。
5、另一台mac的尝试
很高兴的是,通过另一台mac执行代码后,发现输出有了变化:
% 1.sh
??于2
% 1.sh 1 3
??于3
是的,前面是乱码,后面不是乱码了。
我突然意识到哪里有些问题,我重新检视下面这段判定代码,发现可能是“$arg1等”、“$arg1大”等组合形式导致了这个乱码。
if [[ $arg1 -eq $arg2 ]]; then
printf "$arg1等于$arg2"
elif [[ $arg1 -gt $arg2 ]]; then
echo "$arg1大于$arg2"
elif [[ $arg1 -lt $arg2 ]]; then
echo "$arg1小于$arg2"
else
echo "$arg1不等于$arg2"
fi
当我给$arg1和等于之间添加空格时,发现VScode的代码样式都变了。
最后,我重新梳理了一下代码,完整代码如下:
# !/bin/bash
if [ -z "$1" ]; then
arg1="1"
else
arg1="$1"
fi
if [ -z "$2" ]; then
arg2="2"
else
arg2="$2"
fi
if [[ $arg1 -eq $arg2 ]]; then
echo "$arg1 等于 $arg2"
elif [[ $arg1 -gt $arg2 ]]; then
echo "$arg1 大于 $arg2"
elif [[ $arg1 -lt $arg2 ]]; then
echo "$arg1 小于 $arg2"
else
echo "$arg1 不等于 $arg2"
fi
修改的内容为在变量和文字直接添加空格,输出就会正常。
如果不添加空格,就会输出乱码,而这个乱码也是在特定的系统(mac)上才会出现,CentOS就不存在类似的乱码问题。
问题总结
结论上面已经得出来了,Mac的echo命令在输出时,变量和中文之间一定要添加空格,否则就会输出乱码。
本次排查的范围很广、从配置环境到Shell版本,再到重新定位代码和排查终端的相关配置,实际排查和编写这个文本也只是用了一个下午、一个晚上和一个凌晨的时间,也算是补齐了一点Shell的知识,对下次碰到乱码会有一些解决的思路。
关于本次问题的解决也纯属偶然,如果没有另一台mac对比并输出存在差异的话,本次教程也就不复存在,之前的很多教程也是经过多种环境的对比,才能够得出问题的原因。
也算是涨了一点经验,当排查问题没有思路时,应该考虑增加测试变量或设备,来重新定位问题原因。另外,如果确定是代码的问题,就多去尝试修改代码的内容,而不是把代码当作一个范本不去做任何的改变。
以上是本次排查的全部内容,感谢观看。