三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Shell 相关基础入门,在 Ubuntu 与 CentOS Shell 中的语法差异总结(bash、dash、sh)

Shell 相关基础入门,在 Ubuntu 与 CentOS Shell 中的语法差异总结(bash、dash、sh)

新建的test.sh文件,vim进去,每行开头都默认有一个~符号,是什么意思,而且在里面鼠标也失效了?


你问的这两个问题,恰好是初学者刚接触vim编辑器时最常遇到的两个困惑。它们完全正常,不是系统出错了。


一、每行开头的~波浪号是什么意思?

结论:~表示这一行是空白行(没有任何字符,连空格都没有)。

详细解释

符号含义
~(波浪号)缓冲区(buffer)里没有内容的行
普通字母/符号文件实际存在的内容

你的情况:

  • 你用vim test.sh创建了一个新文件

  • 新文件是空的,所以整个屏幕都是空白区

  • vim用~来标记这些空白行,告诉你“这里没有内容”

当你开始输入内容时:

#!/bin/bash echo "hello" ~ ~ ~
  • 第1、2行变成了实际内容(没有~)

  • 第3行及以后仍然是空白行(还有~)

类比:就像一本笔记本,有字的页没有标记,空白页印了一个淡淡的“此页空白”水印。


二、为什么鼠标失效了?

结论:vim默认运行在“纯键盘模式”下,根本不支持鼠标操作。

编辑器鼠标支持
Vim默认不支持(纯键盘操作)
VS Code完全支持
Notepad完全支持
Nano(另一个命令行编辑器)支持鼠标(如果开启)

为什么 Vim 要这样设计?

  1. 历史原因:Vim 诞生于 1991 年,当时鼠标不是标配

  2. 设计哲学:手不离键盘,提高编辑效率(高手用键盘比鼠标快得多)

  3. 远程操作:服务器上根本没有图形界面,鼠标不存在

在 Vim 里怎么操作?

你想要键盘操作(代替鼠标)
移动光标h(左)、j(下)、k(上)、l(右)
跳到行首0
跳到行尾$
删除一个字符x
删除一行dd
复制一行yy
粘贴p
保存并退出ESC→:wq→Enter

如果你实在想用鼠标(不推荐初学者)

在~/.vimrc中添加:

vim

set mouse=a

但这会改变 Vim 的正常行为(选中文本会变成 Visual 模式),反而更容易让初学者困惑。


三、正确的 Vim 学习路径

步骤操作
1用vim test.sh打开文件
2按i键进入插入模式(屏幕底部会显示-- INSERT --)
3用键盘输入内容
4按ESC键退出插入模式
5输入:wq然后按Enter保存并退出

记住 Vim 的两种核心模式:

  • 命令模式(默认):按键盘是各种命令,不能输入文字

  • 插入模式(按i进入):按键盘就是输入文字


四、如果你只是想快速编辑文件(不是学 Vim)

编辑器优点缺点
nano简单直观,底部有快捷键提示功能弱
cat查看文件内容不能编辑
echo >快速写入简单内容只能写,不能改

bash

# 用 nano 编辑(更友好) nano test.sh # 屏幕底部有 ^X Exit 等提示,^ 表示 Ctrl 键 # 用 cat 快速创建 cat > test.sh << 'EOF' #!/bin/bash echo "hello" EOF

总结

你的问题答案
每行开头的~空白行的标记,不是错误
鼠标失效Vim 默认纯键盘,这就是它的设计
怎么输入文字按i进入插入模式
怎么保存退出ESC→:wq→Enter
不想学 Vim 怎么办用nano代替

建议:如果你未来想走大数据开发,Vim 是服务器上必用的工具(因为服务器没有图形界面),值得花一天时间入门。但如果只是偶尔编辑小文件,用nano完全够用。


老师说shell脚本语言的很多语法或设计反人类,尤其我之前作为web前端开发写JavaScript灵活多了,开始学Oracle的时候就觉得这语言太传统了,现在学Shell感觉语法更古老。


请罗列shell语法需要注意的点,或容易错误的点,让我可以平稳过渡。比如空格 双圆括号 中括号 等


你这个感受完全正确。作为前端开发者,你已经被 JavaScript 的灵活性“惯坏了”。Shell 的设计年代(1970年代)决定了它的语法像一台老式手动挡汽车:到处都是坑,而且每个坑都摔过无数人。

下面这张“Shell 语法避坑地图”,专为你这种 JavaScript 背景量身定制。记住,不要试图理解它“为什么反人类”,只需要记住规则并绕过陷阱。


一、最危险的陷阱:空格敏感(JavaScript 最自由,Shell 最严格)

在 JavaScript 中,空格基本无害。在 Shell 中,空格是语法分隔符,多一个少一个都会炸。

场景✅ 正确写法(没空格)解释
变量赋值name="Tom"等号两边绝对不能有空格
条件判断if [ "$a" == "$b" ]括号内部必须有空格,变量最好加引号
函数定义foo()或function foo()括号前后不要乱加空格

bash

# 典型案例(一定会出错) name="Tom" # ✅ 正确 name = "Tom" # ❌ 报错:name: command not found

记忆口诀:赋值等号紧贴变量,比较方括号留空隙。


二、变量的引号地狱(JS 模板字符串真香)

Shell 中双引号、单引号、反引号行为完全不同,而且没有 JS 的模板字符串好使。

引用方式行为
"$VAR"(双引号)变量会展开
'$VAR'(单引号)变量不会展开,纯字面量
`cmd`(反引号)执行命令,获取输出

bash

name="world" echo "hello $name" # hello world(双引号展开) echo 'hello $name' # hello $name(单引号不展开) # 建议:除非你明确需要防止变量展开,否则一律用双引号包裹变量 echo "$name" # ✅ 安全 echo $name # ⚠️ 如果变量有空格会被拆分,容易炸

黄金法则:所有变量都用"$VAR"括起来。


三、条件判断的括号迷阵(JS 一个if走天下)

Shell 有3 种括号,功能完全不同,是新手最容易晕的地方。

语法名称用途
[ ]test 命令字符串/文件判断
[[ ]]扩展 test支持正则、&&、||,推荐
(( ))算术运算数学计算、数值比较
a=5 b=10 # ✅ 数值比较:用 (( )) if (( a < b )); then echo "$a 小于 $b" fi # ✅ 字符串比较:用 [[ ]] name="Alice" if [[ "$name" == "Alice" ]]; then echo "hello Alice" fi # ❌ 错误:用 [ ] 做数值比较会炸(它比较的是字符串) if [ $a < $b ]; then # 错误!

推荐写法:字符串/文件用[[ ]],数字用(( )),忘记古老的[ ]。


四、命令执行与管道(JS 的 Promise 是天堂)

Shell 中命令执行、管道、错误处理非常原始。

需求Shell 写法JS 类比
执行命令并获取输出output=$(ls -l)const output = execSync('ls -l')
上一条命令是否成功$?(0=成功,非0=失败)没有直接类比
逻辑与 (AND)cmd1 && cmd2cmd1 && cmd2
逻辑或 (OR)cmd1 || cmd2cmd1 || cmd2
管道(前输出当后输入)cmd1 | cmd2cmd1.pipe(cmd2)
# 典型的脚本模式 if [ -f "config.txt" ]; then echo "文件存在" else echo "文件不存在,创建中..." touch config.txt fi # 检查命令是否执行成功 grep "error" app.log if [[ $? -eq 0 ]]; then echo "发现错误" fi

五、函数定义与返回值(JS 的函数是瑞士军刀)

Shell 函数非常原始,没有return值(只能用echo输出)。

概念Shell
定义myfunc() { ... }
调用myfunc
参数$1, $2, $@
返回值只能用echo输出
# ✅ 正确:函数返回字符串用 echo get_name() { echo "Alice" } name=$(get_name) # name="Alice" # ❌ 错误:return 只能返回 0-255 的状态码(成功/失败) get_name() { return "Alice" # 错误!return 不是返回值,是退出码 }

记忆:Shell 函数就是一段命令集合,不是 JS 的函数。


六、常见错误速查表(打印出来贴显示器旁)

错误代码原因正确写法
command not found变量赋值有空格name="Tom"
[: too many arguments变量未加引号,有空格if [[ "$var" == "abc" ]]
unexpected operator[ ]里用了==改用[[ "$var" == "abc" ]]
syntax error near unexpected tokenif后少了then或;if [[ ... ]]; then
bad substitution错误的变量语法"${var}"或$(cmd)

七、一份“反人类但必须记住”的模板

#!/bin/bash # 模板:安全、健壮、可读 # 1. 变量:等号无空格,一律加双引号 name="world" greeting="Hello, $name" # 2. 条件:用 [[ ]] 和 (( )) if [[ -f "config.txt" ]]; then echo "config exists" fi if (( 5 > 3 )); then echo "math works" fi # 3. 命令替换:用 $(...),不是反引号 current_date=$(date +%Y-%m-%d) echo "Today is $current_date" # 4. 函数:输出用 echo get_full_name() { echo "Alice Johnson" } full_name=$(get_full_name) # 5. 错误处理 if ! mkdir -p "/tmp/mydir"; then echo "Failed to create directory" exit 1 fi

八、最后:给你的心态建议

感觉现实
“这太原始了”它就是 1970 年代的设计,目的不是优雅,是在资源极度匮乏的环境下稳定运行
“为什么不能像 JS 那样”因为 Shell 是胶水语言,用来粘合各种小命令,不是用来写复杂逻辑的
“我该学到什么程度”能写50 行以内的脚本、能看懂别人写的脚本、能改 bug,就完全够用

一句话总结:不要和 Shell 讲道理,记住它的“奇葩规则”,把它当工具用,写完就跑。你的主业是大数据,不是 Shell 专家。

等你写几天 Shell,再回去写 JS,你会感动得流泪。


Shell在不同Linux版本中语法好像有细微差异,比如$ (($A+$B))有些版本正确,有些版本错误,请使用表格总结常见差异


你的观察非常敏锐!$(( $A + $B ))在某些系统上报错,而在另一些系统上正常,这确实是 Shell 脚本跨平台兼容性的经典问题。核心原因在于:不同 Linux 发行版默认的/bin/sh指向不同的 Shell 实现。


一、问题根源:/bin/sh指向谁?

发行版/bin/sh实际指向特点
Ubuntu / Debiandash轻量、POSIX 严格、启动快、功能少
CentOS / RHELbash功能丰富、支持扩展语法
Alpine Linuxbusybox ash极简、POSIX 子集
macOSbash(旧版)或zsh取决于版本

关键结论:如果你用#!/bin/sh声明脚本,Ubuntu 会用dash执行,而 CentOS 会用bash执行。dash对语法要求更严格,很多bash的“舒服写法”在dash下直接报错。

你遇到的$(( $A + $B ))问题,正是因为dash对算术运算的语法要求更严格。


二、Shell 语法差异速查表

1. 算术运算

语法BashDash/Zsh说明
$(( a + b ))✅✅标准写法,通用
$(( $a + $b ))✅⚠️ 部分版本报错dash对$嵌套敏感
$[ a + b ]✅❌ 报错旧语法,已废弃
let a++✅❌let: not foundlet是 bash 内建命令-2-7
(( a++ ))✅❌ 语法错误C 风格循环同理

✅ 通用写法:result=$((a + b))(内部变量不加$)

# ❌ 在 dash 中可能报错 result=$(( $a + $b )) # ✅ 通用写法 result=$((a + b))

2. 条件判断

语法BashDash/Zsh说明
[ "$a" = "$b" ]✅✅POSIX 标准
[ "$a" == "$b" ]✅❌ 部分报错==是 bash 扩展-7
[[ "$a" == "$b" ]]✅❌ 语法错误[[ ]]是 bash 扩展
[[ $a =~ regex ]]✅❌ 不支持正则匹配是 bash 扩展

✅ 通用写法:[ "$a" = "$b" ](单等号,双括号)


3. 字符串与数组

语法BashDash/Zsh说明
array=(a b c)✅❌ 不支持数组是 bash 扩展-7
${array[1]}✅❌ 不支持同上
${#var}✅✅字符串长度
${var:1:2}✅❌ 不支持子串截取是 bash 扩展-7
${var^^}✅❌ 不支持大小写转换是 bash 扩展-7

✅ 通用替代:用cut、sed、awk或tr处理字符串

# ❌ 子串截取 echo ${str:1:2} # ✅ 通用写法 echo "$str" | cut -c2-3

4. 函数

语法BashDash/Zsh说明
func() { ... }✅✅POSIX 标准
function func() { ... }✅❌ 语法错误function关键字是 bash 扩展-7

✅ 通用写法:func() { ... }(不要写function)


5. 循环

语法BashDash/Zsh说明
for i in {1..10}; do ...✅❌ 不展开{m..n}是 bash 扩展-7
for ((i=0;i<10;i++)); do ...✅❌ 语法错误C 风格循环是 bash 扩展-4-7
select var in list; do ...✅❌ 不支持select是 bash 扩展-7

✅ 通用写法:for i in $(seq 1 10); do ...或while循环

# ❌ dash 不支持 for ((i=0;i<10;i++)); do echo $i; done # ✅ 通用写法 i=0 while [ $i -lt 10 ]; do echo $i i=$((i + 1)) done

6. 重定向与 Here Document

语法BashDash/Zsh说明
<<< "string"✅❌ 不支持Here String 是 bash 扩展-7
>&word(非数字)✅❌ 报错>&err.log写法有问题-7

✅ 通用写法:用 Here Document 或>file 2>&1

# ❌ Here String cat <<< "hello" # ✅ Here Document cat << EOF hello EOF

7. Shebang 与执行方式

场景BashDash/Zsh说明
#!/bin/sh用/bin/sh执行用/bin/sh执行执行器取决于系统-1-5
#!/bin/bash用 bash 执行用 bash 执行前提是系统安装了 bash
sh script.sh忽略 shebang忽略 shebang强制用sh执行
./script.sh按 shebang 执行按 shebang 执行尊重脚本第一行

关键警告:即使用了#!/bin/bash,如果你用sh script.sh执行,会强制使用sh,忽略 shebang-7!


三、算术运算问题详解

针对你提到的$(( $A + $B )):

a=5 b=3 # ❌ 在 dash 中可能报错 result=$(( $a + $b )) # dash 报错:bad substitution # ✅ 通用写法(内部不加 $) result=$((a + b)) # bash 和 dash 都支持 # ✅ 另一种通用写法 result=$(expr $a + $b) # 注意空格,但性能较差[citation:6]

为什么$(( $a + $b ))在 dash 中会报错?

dash的解析器要求$(( ))内部不要使用$引用变量,直接写变量名即可。虽然某些版本的dash可能宽容处理,但为了跨平台兼容,永远不要在内层加$。


四、如何编写跨平台脚本?

方案一:坚持 POSIX 标准(推荐生产环境)

使用#!/bin/sh+仅 POSIX 语法,功能受限但到处能跑。

#!/bin/sh # POSIX 兼容写法 a=5 b=3 result=$((a + b)) # 算术 [ "$a" -eq "$b" ] && echo eq # 比较 for i in $(seq 1 10); do # 循环 echo $i done

方案二:明确使用 bash

用#!/bin/bash声明,并确保脚本以./script.sh或bash script.sh方式执行-1-7。

#!/bin/bash # 大胆使用 bash 扩展 array=(a b c) for ((i=0;i<10;i++)); do echo $i done

方案三:测试你的脚本

# 用 dash 测试(Ubuntu) dash script.sh # 用 bash 测试 bash script.sh # 检查语法 sh -n script.sh

五、一句话总结

你的问题答案
为什么$(( $a + $b ))在某些系统报错?Ubuntu 的/bin/sh是dash,要求算术运算内变量不加$
通用写法是什么?result=$((a + b))
最安全的策略生产环境用#!/bin/sh+ POSIX 语法;复杂功能用#!/bin/bash

我用的Ubuntu,老师用的CentOS,test.sh开头的确老师推荐写的是#!/bin/sh,执行的时候老师用的是 sh test.sh。


我应该是用 bash test.sh执行吗


你的判断完全正确,而且你问到了一个非常关键、生产环境必踩的坑。


结论先行:在你的 Ubuntu 上,应该用bash test.sh执行,或者把脚本第一行改成#!/bin/bash然后用./test.sh执行。


一、为什么老师用sh test.sh没事,你用就会出问题?

环境/bin/sh实际指向sh test.sh的效果
老师的 CentOSbash等于在用bash执行,语法宽松
你的 Ubuntudash在用dash执行,语法严格,很多写法报错

结论:同样写#!/bin/sh+sh test.sh,在你俩机器上实际用的解释器不一样。

这就是为什么:

  • 老师写的脚本在你机器上可能报错

  • 你按老师的习惯写脚本,可能被dash卡住


二、你应该怎么做?(三个方案)

方案做法优点缺点
方案一(推荐学)脚本第一行写#!/bin/bash,执行用./test.sh(需chmod +x)或bash test.sh语法丰富,和老师教的兼容
方案二(最稳妥)脚本第一行写#!/bin/sh,但执行时用bash test.sh不依赖 shebang,强制用 bash每次执行要多敲bash
方案三(不推荐)强行适配dash,只写 POSIX 语法脚本到处能跑语法限制多,很痛苦,不适合学习

三、具体操作示例

#!/bin/bash+./test.sh

#!/bin/bash # 第一行写 bash,而不是 sh a=5 b=3 echo $((a + b)) # 算术 array=(1 2 3) # 数组也可以用 echo ${array[0]}

执行方式:

chmod +x test.sh ./test.sh # 或 bash test.sh

四、验证你的环境(强烈建议亲手跑一下)

# 1. 查看 /bin/sh 指向谁 ls -l /bin/sh # Ubuntu 输出:/bin/sh -> dash # CentOS 输出:/bin/sh -> bash # 2. 测试脚本 cat > test.sh << 'EOF' #!/bin/sh if [[ "a" == "a" ]]; then echo "双等号测试" fi EOF # 3. 用 sh 执行(Ubuntu 会报错) sh test.sh # 报错:test.sh: 2: [[: not found # 4. 用 bash 执行(成功) bash test.sh # 输出:双等号测试

五、学习阶段的建议

阶段建议写法执行方式
跟着老师抄作业脚本里写#!/bin/bash用bash test.sh执行
自己写练习脚本用#!/bin/bash养成好习惯
考试 / 作业提交按老师要求来如果老师用 CentOS,他的sh=bash,你写#!/bin/sh在他那边也能跑

一个现实建议:你可以继续用#!/bin/sh写脚本,但执行时一律用bash test.sh。这样:

  • 语法上你和老师保持一致(他读你脚本不别扭)

  • 执行上你在 Ubuntu 不会踩dash的坑


总结一句话

在 Ubuntu 上,用#!/bin/bash或bash test.sh执行,才能和 CentOS 的行为对齐。


不是说 bash test.sh和sh test.sh一样吗


不一样!这是 Shell 学习中最常见的误解之一,你问到了关键点。


核心结论

执行方式Ubuntu 上实际用的是CentOS 上实际用的是
bash test.shbashbash
sh test.shdash(不是 bash!)bash(sh 指向 bash)

结论:bash test.sh和sh test.sh在 Ubuntu 上不一样,在 CentOS 上才一样。


为什么会有这个误解?

因为很多人(包括老师)在 CentOS 上学习,CentOS 的/bin/sh是软链接指向/bin/bash,所以执行sh test.sh就等于执行bash test.sh。

# CentOS 上 ls -l /bin/sh # 输出:/bin/sh -> bash ← sh 就是 bash # 所以这两个命令完全等价 sh test.sh # 实际执行的是 bash bash test.sh # 也是 bash

但在 Ubuntu 上不一样:

# Ubuntu 上 ls -l /bin/sh # 输出:/bin/sh -> dash ← sh 是 dash,不是 bash! # 这两个命令不等价 sh test.sh # 实际执行的是 dash(语法严格、功能少) bash test.sh # 执行的是 bash(功能丰富)

验证实验(强烈建议你自己跑一遍)

# 1. 创建一个测试脚本 cat > test.sh << 'EOF' #!/bin/sh echo "当前使用的 Shell: $0" if [[ "a" == "a" ]]; then echo "双等号测试通过" else echo "双等号测试失败" fi EOF # 2. 用 sh 执行(Ubuntu 上会报错) sh test.sh # 输出: # 当前使用的 Shell: sh # test.sh: 4: [[: not found ← dash 不认识 [[ # 双等号测试失败 # 3. 用 bash 执行(成功) bash test.sh # 输出: # 当前使用的 Shell: bash # 双等号测试通过 ← bash 认识 [[

为什么 Ubuntu 要把/bin/sh设为dash?

原因说明
性能dash 比 bash 轻量得多,启动快 2-3 倍
标准化dash 严格遵循 POSIX 标准,不添加扩展语法
系统脚本Ubuntu 系统自己的启动脚本都用 dash 执行,加速开机

代价:很多在 bash 里写得很舒服的语法([[ ]]、数组、==、source等),在 dash 里直接报错。


你在 Ubuntu 上应该怎么做?

你的需求推荐做法说明
跟着老师学(CentOS 风格)用bash test.sh执行绕开 dash,行为与 CentOS 一致
自己写脚本练习脚本第一行写#!/bin/bash,然后chmod +x+./test.sh养成好习惯
考试/作业提交用bash test.sh测试通过后再提交确保语法正确
写跨平台脚本只写 POSIX 语法,用sh test.sh测试到处能跑(不推荐初学者)

一句话记忆

Ubuntu 上sh不是bash,是dash。想用 bash 就写bash或#!/bin/bash。

← 返回列表