Things I Learned (Bash)

Progress of dd🔗

Bash

dd 是一个 Bash 命令,可以用于文件/硬盘的整体拷贝。比如,希望将 Raspberry Pi 的 SD 卡复制一份,就可以使用 dd 这个命令来进行。

但是默认的 dd 命令并没有进度提示,在完整执行完之前,默认在 stdout 中不会看到任何输出。

如果想要获得当前 dd 的执行进度,可以尝试如下的一些方法:

  1. 通过 Control + TSIGINFO 发送给 dd 命令,dd 收到后会输出当前的进度信息;
  2. 类似的,也可以通过 pkill 命令将 SIGINFO 发送给 ddpkill -INFO -x dd

其中,针对第二点的命令,可以写一个简单的脚本来定时输出当前的进度:

while pgrep ^dd; do pkill -INFO dd; sleep 10; done

dd 的输出结果示例如下:

1000+0 records in
1000+0 records out
67108864000 bytes transferred in 3.720346 secs (18038339571 bytes/sec)

更多方法(原理都是发送 SIGINFOdd),可以参考这里


Clone SD Card🔗

Bash

Raspberry Pi 的操作系统写在 SD Card 中。如果想将这个当前的系统做克隆(用于备份或存储迁移),可以通过 dd 命令来进行。

  1. 将原始的 SD Card 以及新的 SD Card 插入电脑;
  2. 通过 diskutil 命令来查看当前两张 SD Card 在 dev 中分别的命名是怎样的:
diskutil list

运行后的结果大致如:

/dev/disk2
   #:                       TYPE NAME                    SIZE       IDENTIFIER
  ...

/dev/disk3
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   ...

其中 /dev/disk2/dev/disk3 就分别是插入的两个 SD Card(具体在不同的机器上可能有所不同,需要根据 diskutil 列出的数据进行区分)。

接下来,需要将 /dev/disk3(也就是新的 SD Card)进行 unmount 操作,因为 SD Card 最终要写成的格式并不是 MacOS “理解”的格式(这里只是进行了 unmount,文件系统已经不可访问了,但是物理的 SD Card 依然是系统可以访问的,因而可以被写成任意的格式):

diskutil unmountDisk /dev/disk2

最后,使用 dd 命令进行数据的克隆就可以了:

sudo dd if=/dev/disk2 of=/dev/disk3

当然,如果不需要克隆到新的 SD Card,只是做一个简单的备份,也可以将内容保存到本地的文件中:

sudo dd if=/dev/disk2 of=/path/to/file.dmg

还原备份只需要:

sudo dd if=/path/to/file.dmg of=/dev/disk3

back to previous folder🔗

Bash

在 Bash 中,可以通过以下的命令跳转回上一个访问的目录:

cd -

换句话说,cd - 可以在最后访问的两个目录间来回跳转。


Get Yesterday Date in Bash🔗

Bash

在大多数时候,Mac 系统和 Linux 系统在终端的使用体验上是比较一致的,但偶尔也有一些命令,会出现两端不一样的情况。比如,当需要通过 date 命令获取昨天的日期。

在 Mac 中,可以通过如下的命令来完成:

date -v '-1d' '+%Y-%m-%d'

输出的结果是 2019-08-25。(这里,'+%Y-%m-%d' 指定 date 以“年-月-日”的格式输出日期;另外,如果想要得到明天的日期,可以通过 +1 day+1d 来得到)

然而,在 Linux 系统下,同样的命令无法使用。需要修改成:

date -d '-1 day' '+%Y-%m-%d'

才可以得到同样的结果。这里需要注意一点,如果 Docker 是基于 Alpine 的,默认 date 不支持 -d 这个选项,需要额外安装 coreutils 之后,才可以使用。即,在 Dockerfile 中增加:

RUN apk add --update coreutils && rm -rf /var/cache/apk/*

之后,上面的命令才能正确运行。

如果希望一个命令可以在两个系统中运行,可以用如下的方法进行整合:

[[ $OSTYPE == "darwin"* ]] && \
  date -v '-1d' '+%Y-%m-%d' || \
  date -d '-1 day' '+%Y-%m-%d'

注意,这里需要使用 [[ 进行判断,[ 的比较是无法使用 * 元字符匹配的。当然,这里没有考虑 Windows 的情况,毕竟 Windows 的情况太特殊了,大部分的命令都不兼容。


Raspberry Pi as WOL🔗

Bash

安装 etherwake 用于 WOL (wake on LAN) 操作

sudo apt-get install etherwake

接下来,可以通过命令:

sudo etherwake -i eth0 AA:BB:CC:DD:EE:FF

来唤醒 AA:BB:CC:DD:EE:FF 这个 MAC 地址的设备。几点注意:

  1. etherwake 需要 sudo 运行,否则会报错:etherwake: This program must be run as root.
  2. -i eth0 不是必须的。如果同时有有线和无线网卡,-i 可以强制要求 etherwake 走有线的路径

参考文献


Permission Denied for Rsync🔗

Bash

相比于 scprsync 命令可以在 SSH 拷贝的时候提供更多的灵活性,比如只拷贝新修改的或未存在的文件。

一个简单的拷贝命令如下:

rsync -auv /local/folder host:/remote/folder

这里,-a 表示拷贝所有的文件(包括子文件夹中的),-u 表示只拷贝修改时间更新的部分,-v 则会将结果输出到 stdin 中方便查看。类似的,还可以使用 --ignore-existing 来要求 rsync 只拷贝新的文件,忽略已经存在的部分。

然而在实际使用的过程中,rsync 有如下报错:

Permission denied, please try again.
rsync: connection unexpectedly closed (0 bytes received so far) [sender]
rsync error: error in rsync protocol data stream (code 12) at io.c(235) [sender=3.1.2]

如果换同样的 SSH 配置,使用 scp 就不会有类似的报错,可见本身并不是 SSH 登陆账户权限的问题。这里的 Permission denied 报错非常的具有误导性。实际上,更可能的情况是 rsync 无法在远程主机上找到,需要通过 --rsync-path 参数手动指定。

首先,可以先 SSH 到远程主机上,确认 rsync 本身是存在的:

rsync --help

接着,可以通过 type 命令确认 rsync 的实际位置:

type -a rsync

这里,假设输出的结果是 /bin/rsync,那么,可以将原先的 rsync 命令改写为:

rsync -auv /local/folder host:/remote/folder --rsync-path=/bin/rsync

再次运行就不会报错了。

参考文档


How Makefile works🔗

Bash

在 C 编程中,经常会用到 Makefile 来对源代码进行编译。一个简单的 Makefile 如下:

out: input.c
  $(CC) input.c -o out -Wall -Wextra -std=c99

这里,第一行的 out: input.c 表示 make 应该根据输入 input.c 来产出 out 这个文件。

第二行的 $(CC) 会由 make 替换成本机的 cc 程序(即 c compiler);后面跟着的是 cc 编译会用到的参数,包括输入源文件 input.c,输出文件 out,编译输出所有的 Warning(-WallWarning all-WextraWarning extra),同时指定使用 C99 标准来编译 C 代码(和 ANSI C 相比,C99 允许在函数的任意位置定义变量,而不是必须在顶部)。

运行 make 命令,程序会查找当前目录下的 Makefile 函数,读取其中的配置,根据输入输出的要求,查找文件,然后再选择编译。

第一次编译,程序会用 input.c 编译出一个 out 文件来。

input.c 没有修改的情况下,如果再运行一次 make 命令,会得到如下的输出:

make: `out` is update to date.

这里,make 程序并没有通过任何外部文件的方式记录编译的情况。判断是否需要编译完全依赖于系统默认的文件功能,即简单的比较 input.cout 两个文件的最后修改时间。如果 out 的最后修改时间比 input.c 要晚,就认为 out 是最新的,不再重复编译;如果 input.c 的最后修改时间晚于 out 的时间,或是 out 压根就不存在,那么 make 就会执行 Makefile 中配置的编译命令。

可以通过以下方式欺骗 Makefile 来检查这一行为:

  1. 修改一下 input.c 并保存
  2. 删除 out 文件,然后用 touch 命令创建一个空的 out 文件。因为是先修改,再创建,所以 out 的创建时间会晚于 input.c
  3. 尝试执行 make 命令,会发现提示 out 已经是最新的,并没有执行真正的编译命令(尽管这里 out 并不是通过 make 编译出来的)

Check Exit Code of Command🔗

Bash

在命令行中,一个命令会有一个返回数值,0 代表正确运行;如果命令返回了非 0 数据,则代表命令运行出现了错误。

比如,如果 Jest 命令跑单元测试出现了错误,那么就会返回一个非 0 的值。运用 set -e 可以让 Bash 在遇到非零返回的命令行之后即停止,不再运行接下去的命令。

那么,该如何确定之前的命令是否返回了 0 呢?

可以简单的使用如下的命令:

echo $?

这里的 $? 就是上一个命令返回的数值。如果上一条命令执行成功,那么这里应该输出 0


Exit when Command Fail🔗

Bash

在写 CI 脚本的时候,希望可以在脚本执行失败之后终止后续的所有操作。比如:

echo "start"
yarn test
echo "end"

如果 yarn test 这个命令失败了,希望不执行 echo "end" 语句。然而通过执行上面的代码,会发现默认是执行的。如果希望不执行这个操作,有几种思路:

第一种,是用 && 将语句串联起来,比如:

echo "start" && yarn test && echo "end"

这样的方案,缺点是比较的麻烦。一旦东西比较多,就很难保证代码的可读性了。

第二种方案,是使用 set -e,脚本改为:

set -e
echo "start"
yarn test
echo "end"

如此一来,脚本在语句执行失败(Exit Code 不是 0)之后就会退出,不会执行接下去的脚本。

参考文档


Bad owner or permissions🔗

Bash

在 Docker 中使用 SSH 的功能时,发现 SSH 报错:

Bad owner or permissions on ~/.ssh/config

通过 ls -l 查看 ~/.ssh/config,得到如下结果:

-rw------- 1 1000  1000   557 Jul 29 20:32 config

注意到给出的 User 和 Group 的值不是一个名字(如 root),而是一个数字。这说明,文件所属的 User / Group 无法找到。

可以通过如下的命令查看当前 root 用户的 ID:

id -u root # output => 0

可以看到和 ls 列出的 ID 是不匹配的。这说明,导致 SSH 无法正常工作的主要原因,是 ~/.ssh/config 文件权限的设置有问题。可以通过如下的命令将权限分配给当前的 root 用户:

chown -R root:root /root/.ssh

再次运行 SSH 就可以正常工作了。


SSH Host Config🔗

Bash

如果手上有多台设备在管理,SSH 的时候需要记住各个设备的 IP 地址、输入,总是很麻烦的。SSH 提供了配置文件的功能,可以为不同的 IP 设置别名,同时配置登陆需要用到的用户名和 RSA 私钥等。

配置方法

修改 ~/.ssh/config 文件,增加每个设备对应的配置数据。举例如下:

Host pi
    Hostname 192.168.xx.xx
    User pi
    IdentityFile ~/.ssh/id_pi_rsa

这样就配置好了一个 Raspberry Pi 的别名。接下来,可以直接使用如下的命令来访问设备:

ssh pi

除了 SSH 之外,SCP 也可以使用同样的配置。比如:

scp -r /local/path pi:/remote/path

Command `tldr`🔗

Bash

tldr 是一个社区维护的命令工具,可以用于输出某一个命令的说明文档。相比于 man 详尽的文档,tldr 更侧重于例子。通过具体的使用场景,介绍该命令一些常用的方法。

tldr 相关的一些链接地址:GitHub官方网站

安装 tldr 可以使用 brew

brew install tldr

比如,需要了解 tar 的使用方法,可以运行下面的命令:

tldr tar

运行的结果如下:

tldr command result

注:tldr 的全称是 Too Long; Don’t Read。可以理解成“摘要”的意思。


SSH ProxyJump🔗

Bash

ssh 自带跳板机功能:-J。示例代码如下:

ssh -J userA@a.xxx.com userB@b.xxx.com

命令会需要依次输入 a.xxx.com 和 b.xxx.com 两台机器的登陆信息。校验通过之后,就会登陆 b.xxx.com 这台机器,登陆的用户是 userB。并且,登陆是通过 a.xxx.com 这台机器的 userA 完成的。a.xxx.com 在这里就是一个跳板机的功能。

参考文档


glob🔗

Bash

Glob 类似于 Regular Expression,主要的使用场景是用于批量的文件匹配,在 bash 或是配置文件中常常被使用。下面列举了一些常见的语法规则:

  • * 匹配任意多个字符(包括匹配零个)
  • ? 匹配任意一个字符
  • [abc] 匹配方括号中的任意一个字符
  • [!abc][^abc] 匹配除了方括号中定义的三个字符外的任意字符
  • [a-z] 匹配方括号定义范围内的任意一个字符
  • [!a-z][^a-z] 匹配除了方括号定义范围内的任意一个字符
  • {ab,cd,ef} 匹配花括号中定义的三个字符串中的任意一个

举个例子,如果 Jest 的单元测试文件命名规范的正则表达式是:.+\.(?:test|spec)\.[tj]sx?$,也就是匹配下面的这些文件:

  • a.test.js
  • b.test.jsx
  • c.test.ts
  • d.test.tsx
  • e.spec.js
  • f.spec.jsx
  • g.spec.ts
  • h.spec.tsx

那么,相应的 Glob 可以写:*.{test,spec}.{js,jsx,ts,tsx}

如果不涉及到 React 的代码(没有 jsx),可以写成:*.{test,spec}.[tj]s

参考文档:


Upgrade Npm Dependencies🔗

Bash

npmyarn 都提供升级依赖的命令。

针对 npm,可以使用 npm update 来执行,命令格式如下:

npm update [-g] [<pkg>...]

更新的时候,默认会更新 package.json 文件,可以通过增加 --no-save 标记来禁用这一改动。

npm 的文档可以看这里

yarn 的命令会更加丰富一些,命令格式如下:

yarn upgrade [package | package@tag | package@version | @scope/]... [--pattern]

其中,--pattern 后面可以跟 grep 的 pattern,只有匹配到的依赖会被升级。

默认情况下,升级会参考 package.json 里定义的依赖允许的升级范围来选择可行的最高版本进行升级。如果希望直接升级到最新版本(往往意味着会有 breaking change),那么可以加上 --latest 标志。

yarn 的文档可以看这里


Recursively delete files by type🔗

Bash

以下 Bash 代码可以递归删除指定的 .map 文件。

find . -type f -name '*.map' -delete

如果同时希望删除 .map.xxx 文件,可以加上 -o flag

find . -type f -name '*.map' -o -name '.*' -delete

一些参数说明:

  • -type f 表示需要查找的是文件
  • -name 'xxx' 定义需要匹配的文件名
  • -o 表示 or,后面可以跟新的匹配规则
  • -delete 表示匹配到的文件需要被删除

Disk Usage of Folder🔗

Bash

如果需要对目录下文件的占用空间做排序,可以使用下面的命令:

du -d 3 -k | sort -h

其中,du -d 3 表示,最多显示三层子目录,-k 会让输出以 KB 作为单位。sort -h 会对结果进行排序,排序的依据是文件夹的大小。这里,排序需要带上 -h 的标识位,不然以字符串进行排序的话,输出没有意义(比如,100 会排在 9 的前面)。


Open Application in Terminal🔗

Bash

在 Mac 系统里面,.app 程序本质上就是一个目录,里面包含了很多文件。如果直接在 Terminal 输入 .app 的地址,会进入这个目录,而不是运行这个 App。如果需要运行,可以使用下面的命令:

open /Application/Example.app

如果需要指定 NODE_ENV 等信息,就可以一起配合使用

NODE_ENV=development open /Application/Example.app