前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版

shell入门,从变量语法到能上生产的脚本

首页2018-12-24 10:32:41Back-End
LinuxShellBash脚本

前端写多了迟早要碰 shell。CI 里的构建步骤、服务器上的发布脚本、清理日志的定时任务,最后都会落到一个 .sh 文件里。语法看起来简单,if、for、函数都有,跟 JavaScript 一比好像还更少东西要记。

然后你就会遇到这样的事:脚本里某一步明明失败了,后面的步骤还是照常往下跑,最后部署了一个空目录上去;或者路径里带了个空格,rm -rf $DIR 直接把不该删的删了。这些不是你逻辑写错了,是 shell 的默认行为跟大多数语言不一样,它默认「出错也继续」,默认「变量展开后再做一次分词」。

这篇把 shell 编程从变量、条件、循环、函数一路理下来,每个语法点都说清楚它在什么场景用、边界在哪。最后单独用一节讲怎么把一个能跑的脚本变成一个不会坑你的脚本。

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • shell 的种类,#! 那一行到底做了什么,两种执行方式的差别
  • 变量的定义、只读、删除,以及三种变量作用域
  • 字符串和数组的常用操作,单双引号的关键区别
  • 位置参数 $1、$#、$@ 和 $*,以及它们在函数里的复用
  • 四类运算符,expr、[ ]、[[ ]]、$(( )) 该用哪个
  • echo 和 printf 的取舍,为什么脚本里推荐 printf
  • test 命令的数值、字符串、文件三类测试
  • if、for、while、until、case 以及 break/continue
  • 函数的定义、传参和返回值,return 和 echo 的区别
  • 重定向和管道,2>&1 的写法顺序为什么不能反
  • set -euo pipefail 每个字母在防什么
  • 变量不加引号会出什么事,以及几个高频踩坑点

# 一、初识shell

# 1.1 Shell 环境

Shell 是包在内核外面的一层命令解释器,你敲的每一条命令都由它翻译后交给系统执行。Linux 的 Shell 种类众多,常见的有

  • /usr/bin/sh或/bin/sh
  • /bin/bash
  • C Shell(/usr/bin/csh)
  • K Shell(/usr/bin/ksh)
  • Shell for Root(/sbin/sh)

Bash 在日常工作中被广泛使用,同时它也是大多数 Linux 系统默认的 Shell。

这里有个坑值得先说。/bin/sh 在很多系统上是指向别的 shell 的软链接,Debian 和 Ubuntu 上它指向的是 dash,一个精简的 POSIX shell。你在 bash 里跑得好好的脚本,换成 sh script.sh 执行可能就报语法错误,因为 [[ ]]、数组、+= 这些都是 bash 扩展,dash 不支持。这个我排查过一下午,最后发现只是因为 CI 的镜像里 sh 不是 bash。

用 vi/vim 新建一个文件 test.sh,扩展名为 sh(sh 代表 shell)。扩展名并不影响脚本执行,见名知意就好。

#!/bin/bash
echo "Hello World !"
@前端进阶之旅: 代码已经复制到剪贴板

#! 是一个约定的标记,通常叫 shebang,它告诉系统这个脚本需要什么解释器来执行。echo 命令用于向窗口输出文本。

要写 #!/bin/bash 还是 #!/usr/bin/env bash?后者会去 PATH 里找 bash,在 bash 装在非标准路径的系统(比如 macOS 上用 Homebrew 装的新版 bash)上更通用。脚本只在自家服务器上跑,写死路径没问题;要发出去给别人用,env 那种写法更保险。

# 1.2 运行 Shell 脚本有两种方法

作为可执行程序

chmod +x ./test.sh  # 使脚本具有执行权限
./test.sh  # 执行脚本
@前端进阶之旅: 代码已经复制到剪贴板

注意一定要写成 ./test.sh,而不是 test.sh。运行其它二进制程序也一样,直接写 test.sh 的话,linux 系统会去 PATH 里找有没有叫 test.sh 的东西,而 PATH 里通常只有 /bin、/sbin、/usr/bin、/usr/sbin 这些目录,你的当前目录不在里面,所以会报「找不到命令」。写成 ./test.sh 是在告诉系统,就在当前目录找。

为什么当前目录不在 PATH 里?这是个安全设计。要是当前目录在 PATH 里,别人往共享目录放一个叫 ls 的恶意脚本,你 cd 进去随手敲个 ls 就中招了。

作为解释器参数

这种方式是直接运行解释器,把脚本文件名当参数传给它。

/bin/sh test.sh
/bin/php test.php
@前端进阶之旅: 代码已经复制到剪贴板

这么跑的脚本不需要在第一行指定解释器信息,写了也不起作用,因为解释器已经在命令行里定死了。同理,这种方式也不需要给脚本加执行权限。

还有第三种执行方式值得提一下,source test.sh(或者简写 . test.sh)。前两种都是开一个子进程去跑脚本,脚本里定义的变量在脚本结束后就没了;source 是在当前 shell 里执行,变量会留下来。所以配环境变量的脚本必须用 source,而不是 ./。「我脚本里明明 export 了,退出来怎么就没了」这个问题,答案就在这。

# 二、shell变量

变量的命名规则和大多数语言差不多,但有一条是 shell 独有的,赋值时等号两边不能有空格。

  • 定义变量时,变量名不加美元符号
  • 变量名和等号之间不能有空格,等号和值之间也不能有
  • 命名只能使用英文字母、数字和下划线,首个字符不能以数字开头
  • 中间不能有空格,可以使用下划线(_)
  • 不能使用标点符号
  • 不能使用 bash 里的关键字(可用 help 命令查看保留关键字)
your_name="poetries" # 等号两边都不能有空格
@前端进阶之旅: 代码已经复制到剪贴板

为什么这么严格?因为 shell 是按空格切分命令的。写成 your_name = "poetries",shell 会理解成「执行 your_name 这个命令,给它两个参数 = 和 poetries」,然后报 command not found。这个规则和 [ $a == $b ] 里必须有空格的规则看起来矛盾,其实是同一个道理,[ 本身就是个命令,它的参数当然要用空格分开。

有效的 Shell 变量名示例如下。

RUNOOB
LD_LIBRARY_PATH
_var
var2
@前端进阶之旅: 代码已经复制到剪贴板

# 2.1 使用变量

使用一个定义过的变量,只要在变量名前面加美元符号即可。

your_name="poetries"
echo $your_name
echo ${your_name}
@前端进阶之旅: 代码已经复制到剪贴板

变量名外面的花括号是可选的,加不加都行,加花括号是为了帮助解释器识别变量的边界。看下面这个例子。

for skill in Ada Coffe Action Java; do
    echo "I am good at ${skill}Script"
done
@前端进阶之旅: 代码已经复制到剪贴板

如果不给 skill 加花括号,写成 echo "I am good at $skillScript",解释器会把 $skillScript 整个当成一个变量名(它没被定义过,值为空),输出就成了 I am good at,跟预期完全不一样。

推荐给所有变量都加上花括号,这是个好习惯。花括号还解锁了一批很实用的展开语法,后面几节的字符串截取、长度获取都建立在它上面。

已定义的变量可以被重新定义。

your_name="tom"
echo $your_name
your_name="alibaba"
echo $your_name
@前端进阶之旅: 代码已经复制到剪贴板

# 2.2 只读变量

使用 readonly 命令可以把变量定义为只读,之后它的值就不能再改了。下面这个例子尝试更改只读变量,结果会报错。

#!/bin/bash
myUrl="http://www.w3cschool.cc"
readonly myUrl
myUrl="http://www.runoob.com"
@前端进阶之旅: 代码已经复制到剪贴板
/bin/sh: NAME: This variable is read only.
@前端进阶之旅: 代码已经复制到剪贴板

这个特性在脚本里挺有用。把不该被改的配置项(部署根目录、备份保留天数)声明成 readonly,后面代码里手滑覆盖掉的时候会直接报错,而不是带着错误的值一路跑下去。有点像 JavaScript 里的 const,只不过 shell 里没人强制你用。

# 2.3 删除变量

使用 unset 命令可以删除变量,语法是 unset variable_name。变量被删除后不能再次使用,另外 unset 命令不能删除只读变量。

#!/bin/sh
myUrl="http://www.runoob.com"
unset myUrl
echo $myUrl
@前端进阶之旅: 代码已经复制到剪贴板

要说明的是,shell 里「变量不存在」和「变量为空字符串」是两种状态,但 echo 出来都是空的,肉眼分不出来。真要区分得用 ${var+x} 这类展开语法,或者在开头 set -u(第十二节会讲),让引用未定义变量直接报错。

# 2.4 变量类型

运行 shell 时会同时存在三种变量。

局部变量 在脚本或命令中定义,仅在当前 shell 实例中有效,其他 shell 启动的程序不能访问它。

环境变量 所有程序都能访问,包括 shell 启动的子程序。有些程序需要环境变量才能正常运行,必要的时候脚本里也可以自己定义环境变量。

shell 变量 由 shell 程序自己设置的特殊变量,其中一部分是环境变量,一部分是局部变量,这些变量保证了 shell 的正常运行。

三者的实际区别在于「子进程能不能看到」。普通赋值 FOO=bar 只在当前 shell 有效,加上 export FOO=bar 之后它才会被子进程继承。你在脚本里设了个变量,然后调用另一个脚本却读不到,多半就是漏了 export。

FOO=bar           # 只有当前 shell 看得见
export FOO=bar    # 子进程也能看见
env | grep FOO    # 确认它进了环境变量表
@前端进阶之旅: 代码已经复制到剪贴板

# 2.5 Shell 字符串

字符串是 shell 编程中最常用的数据类型(除了数字和字符串,也没什么别的类型好用了)。字符串可以用单引号,也可以用双引号,还可以不用引号,三种写法的行为完全不同。

单引号

str='this is a string'
@前端进阶之旅: 代码已经复制到剪贴板

单引号里的任何字符都会原样输出,里面的变量不会被展开。单引号字符串中也不能出现单引号,对单引号用转义符也不行。

双引号

author='poetries'
echo "hello,I'm ${author}"
@前端进阶之旅: 代码已经复制到剪贴板

双引号里可以有变量,可以出现转义字符。

这个区别在写脚本时非常关键,一句话记:要变量展开用双引号,要一字不差的原文用单引号。传给 awk、sed 的表达式里全是 $1、$2 这种符号,必须用单引号包住,不然 shell 会先把它们当成自己的变量给替换掉,你会拿到一个空表达式。

至于不加引号,那是最危险的一种,第十二节会专门讲它会出什么事。

# 2.6 拼接字符串

shell 里没有拼接运算符,把两个东西写在一起就是拼接。

your_name="poetries"
greeting="hello, "$your_name" !"
greeting_1="hello, ${your_name} !"
echo $greeting $greeting_1
@前端进阶之旅: 代码已经复制到剪贴板

两种写法结果一样,但第二种可读性明显更好。第一种靠引号的开合来切分,多拼几段之后自己都数不清哪个引号配哪个。拼路径的时候尤其推荐第二种,"${BASE_DIR}/${APP_NAME}/dist" 一眼就看得出结构。

fe
  • 一、初识shell
    • 1.1 Shell 环境
    • 1.2 运行 Shell 脚本有两种方法
  • 二、shell变量
    • 2.1 使用变量
    • 2.2 只读变量
    • 2.3 删除变量
    • 2.4 变量类型
    • 2.5 Shell 字符串
    • 2.6 拼接字符串
    • 2.7 获取字符串长度
    • 2.8 提取子字符串
    • 2.9 Shell 数组
    • 2.10 定义数组
    • 2.11 读取数组
    • 2.12 获取数组的长度
    • 2.13 Shell 注释
  • 三、shell传递参数
  • 四、数组
    • 4.1 读取数组
    • 4.2 获取数组中的所有元素
    • 4.3 获取数组的长度
  • 五、基本运算符
    • 5.1 算术运算符
    • 5.2 关系运算符
    • 5.3 布尔运算符
    • 5.4 逻辑运算符
    • 5.5 字符串运算符
    • 5.6 文件测试运算符
  • 六、echo用法
    • 6.1 显示普通字符串
    • 6.2 显示转义字符
    • 6.3 显示变量
    • 6.4 显示换行
    • 6.5 显示不换行
    • 6.6 显示结果定向至文件
    • 6.7 显示命令执行结果
  • 七、printf用法
  • 八、test命令
    • 8.1 数值测试
    • 8.2 字符串测试
    • 8.3 文件测试
  • 九、流程控制if/while/case
    • 9.1 if
    • 9.2 if else
    • 9.3 if else-if else
    • 9.4 for 循环
    • 9.5 while 语句
    • 9.6 until 循环
    • 9.7 case
    • 9.8 跳出循环
    • 9.9 continue
  • 十、函数

← Typescript基础及结合React实践(一) 从零搭出可跑的项目nodejs系列之express中间件机制与路由用法详解 →