n的解析


#Linux#


2015-11-06

n是TJ大神为node设计的版本管理工具,使用shell编写,是用来学习和巩固shell的很不错的例子。

源码地址: https://github.com/tj/n

源码注释: https://github.com/letiantian/n-source

设计与实现

文件存放位置

n会把下载的各个版本的node放到/usr/local/n/versions/目录中。当指定某个版本的node时,对应的node、npm命令会复制到/usr/local/bin/目录中,lib、include等文件也会复制到/usr/local/下对应的文件中。而全局安装的package会放到/usr/local/lib/node_modules/目录下。

node有一个基本目录,再这个基本目录下,各个文件的布局是固定的。查看基本目录:

$ npm get prefix
/usr/local

n的help

$ n -h

  Usage: n [options/env] [COMMAND] [args]

  Environments:
    n [COMMAND] [args]            Uses default env (node)
    n io [COMMAND]                Sets env as io
    n project [COMMAND]           Uses custom env-variables to use non-official sources

  Commands:

    n                              Output versions installed
    n latest                       Install or activate the latest node release
    n -a x86 latest                As above but force 32 bit architecture
    n stable                       Install or activate the latest stable node release
    n <version>                    Install node <version>
    n use <version> [args ...]     Execute node <version> with [args ...]
    n bin <version>                Output bin path for <version>
    n rm <version ...>             Remove the given version(s)
    n --latest                     Output the latest node version available
    n --stable                     Output the latest stable node version available
    n ls                           Output the versions of node available

  (iojs):
    n io latest                    Install or activate the latest iojs release
    n io -a x86 latest             As above but force 32 bit architecture
    n io <version>                 Install iojs <version>
    n io use <version> [args ...]  Execute iojs <version> with [args ...]
    n io bin <version>             Output bin path for <version>
    n io rm <version ...>          Remove the given version(s)
    n io --latest                  Output the latest iojs version available
    n io ls                        Output the versions of iojs available

  Options:

    -V, --version   Output current version of n
    -h, --help      Display help information
    -q, --quiet     Disable curl output (if available)
    -d, --download  Download only
    -a, --arch      Override system architecture

  Aliases:

    which   bin
    use     as
    list    ls
    -       rm

处理命令行参数

下面是n处理参数的代码:

if test $# -eq 0; then    # 参数个数为0
  test -z "$(versions_paths)" && err_no_installed_print_help   # versions_paths是函数
  display_versions
else
  while test $# -ne 0; do
    case $1 in
      -V|--version) display_n_version ;;    # display_n_version函数里有exit
      -h|--help|help) display_help; exit ;;
      -q|--quiet) set_quiet ;;
      -d|--download) ACTIVATE=false ;;
      --latest) display_latest_version; exit ;;
      --stable) display_latest_stable_version; exit ;;
      io) set_default $1 ;; # set bin and continue
      project) DEFAULT=2 ;;
      -a|--arch) shift; set_arch $1;; # set arch and continue
      bin|which) display_bin_path_for_version $2; exit ;;
      as|use) shift; execute_with_version $@; exit ;;
      rm|-) shift; remove_versions $@; exit ;;
      latest) install_latest; exit ;;
      stable) install_stable; exit ;;
      ls|list) display_remote_versions; exit ;;
      *) install $1; exit ;;
    esac
    shift
  done
fi

$#是参数数量,$@是参数列表。

如果运行n时,没有给出任何参数,n要尝试显示当前已经安装的所有版本的node。如果没有安装任何node,则报错,并退出程序。如果有若干版本,则全屏显示,可以用上下方向键选择某一版本,回车后该版本的node生效。

假如第一个参数为-Vcase代码块中匹配-V后会去执行display_n_version函数,然后break(即;;)。出了case代码块,通过shift从参数列表从删除位于第一个位置的参数-V,然后继续处理其他参数。

假如现在第一个参数是io,则意味着要处理的不是默认的node,而是iojs,set_default io会将这一设置放入脚本的全局变量DEFAULT中,之后安装、删除等操作都是针对iojs的。为方便介绍,下文中都不考虑io这一情况。

假如现在第一个参数是which,则第2个参数应该是版本号,n会调用display_bin_path_for_version函数将该版本号的node的路径输出:

$ n bin 4.2.2
/usr/local/n/versions/node/4.2.2/bin/node 
$ n bin 4.2.3

  Error: 4.2.3 is not installed

输出后程序结束。

假如现在第一个参数是use,则下一个参数应该是版本号,此时会执行指定版本号的node或者iojs:

$ n use 4.2.2
> 1+1
2

然后退出程序。

假如现在第一个参数是lsDEFAULT仍然指向默认的node,display_remote_versions函数会被调用,该函数拉取https://nodejs.org/dist/的网页源码,把所有的版本信息提取出来并输出,其中本地安装的版本和正在使用的版本的字体颜色会不一样。然后,程序退出。

假如现在第一个参数与给定的那些关键字都不同,则认为是一个版本号,n应该尝试去安装它。

查找指定的主、次版本号的对应的最新版本

node的安装文件是在https://nodejs.org/dist/下载的。假如现在我们要查找4.2的最新版本,过程如下:

1、获取https://nodejs.org/dist/网页源码: 可以用curl或者wget,这里用curl。

$ curl -s https://nodejs.org/dist/
<html>
<head><title>Index of /dist/</title></head>
<body bgcolor="white">
<h1>Index of /dist/</h1><hr>

<a href="latest/">latest/</a>                                            29-Oct-2015 21
:04                   -
...........省略.........
<a href="v0.10.2/">v0.10.2/</a>                                           03-Apr-2013 0
5:01                   -
<a href="v0.10.20/">v0.10.20/</a>                                          09-Oct-2013 
17:25                   -
...........省略.........   
<hr></body>
</html>

2、提取所有的链接 使用egrep命令将所有含链接的行提取出来。

$ curl -s https://nodejs.org/dist/ | egrep '</a>'
...........省略.........
<a href="v0.9.9/">v0.9.9/</a>                                            14-Oct-2015 10:38                   -
<a href="v4.0.0/">v4.0.0/</a>                                            08-Sep-2015 22:08                   -
...........省略.........

egrep就是grep -E

3、把每一行的版本号提取出来 继续用egrep把所有版本号提取出来。

$ curl -s https://nodejs.org/dist/ | egrep '</a>' | egrep -o '[0-9]+\.[0-9]+\.[0-9]+'
...........省略.........
0.8.3
0.8.4
0.8.4
0.8.5
...........省略.........
0.9.9
4.0.0                  -
...........省略.........

4、去除0.0~0.7、0.8.0~0.8.5这些版本

$ curl -s https://nodejs.org/dist/ | egrep '</a>' | egrep -o '[0-9]+\.[0-9]+\.[0-9]+' | egrep -v '^0\.[0-7]\.' | egrep -v '^0\.8\.[0-5]$' 

5、去重,排序 使用sort命令对版本号去重、排序。

$ curl -s https://nodejs.org/dist/ | egrep '</a>' | egrep -o '[0-9]+\.[0-9]+\.[0-9]+' | egrep -v '^0\.[0-7]\.' | egrep -v '^0\.8\.[0-5]$' | sort -u -k 1,1n -k 2,2n -k 3,3n -t .

6、找到以4.2开头的所有版本

$ curl -s https://nodejs.org/dist/ | egrep '</a>' | egrep -o '[0-9]+\.[0-9]+\.[0-9]+' | egrep -v '^0\.[0-7]\.' | egrep -v '^0\.8\.[0-5]$' | sort -u -k 1,1n -k 2,2n -k 3,3n -t . | egrep "^4.2"
4.2.0
4.2.1
4.2.2

7、使用tail得到最新的版本号

$ curl -s https://nodejs.org/dist/ | egrep '</a>' | egrep -o '[0-9]+\.[0-9]+\.[0-9]+' | egrep -v '^0\.[0-7]\.' | egrep -v '^0\.8\.[0-5]$' | sort -u -k 1,1n -k 2,2n -k 3,3n -t . | egrep "^4.2" | tail -n 1
4.2.2

8、把以上命令转换成shell脚本

# !/usr/bin/env bash
prefix=4.2
version=$(curl -s https://nodejs.org/dist/ 2> /dev/null \
      | egrep "</a>" \
      | egrep -o '[0-9]+\.[0-9]+\.[0-9]+' \
      | egrep -v '^0\.[0-7]\.' \
      | egrep -v '^0\.8\.[0-5]$' \
      | sort -u -k 1,1n -k 2,2n -k 3,3n -t . \
      | egrep ^$prefix \
      | tail -n1)
echo $version

安装某一版本

安装某一版本时,调用的是install函数,参数是版本号。

主要流程如下:

  1. 如果版本号只给出了主、次版本,没有给修订版本,则拉取最新的修订版本号。
  2. 若该版本对应的目录存在,且目录下没有n.lock文件(有则意味着正在被其他运行的n安装),意味着该版本已经被安装,根据其他条件判断是否激活该版本,然后退出。
  3. 生成该版本对应的安装文件的url,判断该url是否有效(返回200),无效则退出。
  4. 创建该版本对应的目录,并在该目录下创建文件n.lock,代表正在安装。下载文件,解压到该目录。
  5. 删除n.lock,安装完成。
  6. 根据其他条件判断是否激活该版本,然后退出。

这里非常有意思的是用n.lock代表安装状态。

如何进入全屏、退出全屏

下面的例子可以用来理解如何进入全屏、退出全屏:

# !/usr/bin/env bash
enter_fullscreen() {
  tput smcup    # Save the display
  stty -echo    # 不回显输入的字符
}

leave_fullscreen() {
  tput rmcup    # Restore the display
  stty echo     # 回显输入的字符
}

enter_fullscreen
echo "已经进入全屏"
sleep 12

leave_fullscreen

捕捉方向键

方向键对应长度为3的特殊字符,可以在Input Translation找到这些按键的字符标识。

下面是一个示例:

# !/usr/bin/env bash
while true; do
    read -n 3 c   # 读取3个字符放入变量c中
    case "$c" in
      $'\033[A')
        echo "上方向键"
        ;;        # 相当于break
      $'\033[B')      # 下方向键
        echo "下方向键"
        ;;
      $'\033[D')      # 下方向键
        echo "左方向键"
        ;;
      $'\033[C')      # 下方向键
        echo "右方向键"
        ;;
      *)          # 默认
        echo $c   # 如果直接回车,$c是空字符串
        echo "退出程序"
        exit
        ;;
    esac
done

如何退出上面的程序呢?
方法1是按回车键。 方法2是依次按三个数字或者字母或者其他字符,只要达到三个,就会退出。

一个小问题,回车键如何捕捉,见下面的代码:

# !/usr/bin/env bash
while true; do
    read -n 3 c  
    case "$c" in
      "")
        printf "\t enter \n"
        ;;        
      *)
        exit
        ;;
    esac
done

如何激活某个版本的node

这里的“激活”就是指将某一版本的node、npm等文件从/usr/local/n/versions/拷贝到/usr/local/bin,其他的相关文件也拷贝到对应目录的动作。

在n中,该功能由display_versions函数实现。

在终端输入n,回车后会进入全屏:

此时,通过执行check_current_version函数得到当前使用的node版本,通过versions_paths函数得到本机安装的所有node版本,并排序。display_versions_with_selected函数用来显示本机所有的node版本,并高亮目前在使用的版本。

然后在一个while循环里监视键盘的输入,如果是上方向键或者下方向键选择其他的版本,就清屏,用display_versions_with_selected函数用来显示本机所有的node版本,并高亮目前选择的版本。

如果输入的是其他字符,则激活当前选中的版本,退出程序。

删除某个版本的node

删除操作由remove_versions函数实现。思路很简单,如果要删除的版本和当前在使用的版本不一致,则直接在/usr/local/n/versions/目录中删除对应版本的整个目录;若一致,则不删除。

remove_versions函数可以接受多个版本号作为参数。

隐藏/显示光标、删除当前行

先看下面的示例:

代码为:

# !/usr/bin/env bash
hide_cursor() {
  printf "\e[?25l"
}

show_cursor() {
  printf "\e[?25h"
}

erase_line() {
  printf "\033[1A\033[2K"
}

hide_cursor
echo "hide_cursor"

sleep 4
erase_line

show_cursor
echo "show_cursor"

sleep 4
erase_line

这里面就是特殊控制字符的使用。在erase_line函数中,033是八进制,十进制值是27,在ASCII表中代表特殊字符ESC (escape),转义的意思。 \033[1A是将光标移动到前一行, \033[2K是清除光标所在行。

资料:
Terminal codes (ANSI/VT100) introduction
ANSI questions: “... x1B ..?25h” and ... x1BE”
ANSI escape code
Using read without triggering a newline action on terminal

n的Makefile

下面是n的Makefile:

PREFIX ?= /usr/local

install: bin/n
    mkdir -p $(PREFIX)/$(dir $<)
    cp $< $(PREFIX)/$<

uninstall:
    rm -f $(PREFIX)/bin/n

.PHONY: install uninstall

变量赋值

?=是赋值操作符,如果PREFIX已经有值就仍为原值,否则就用/usr/local

关于赋值,可以参考Makefile 中:= ?= += =的区别

dir命令

dir用于列出目录中内容,例如:

$ dir /usr/
bin  games  include  lib  local  NX  sbin  share  src
$ dir /etc/passwd
/etc/passwd

不过在Makefile里,$(dir path/to/file)是用来获取目录的。这个可以参考8.3 Functions for File Names

例如下面的Makefile:

test: 
    echo $(dir /etc/hosts)
    $(dir /etc/hosts)

执行结果如下:

$ make test 
echo /etc/
/etc/
/etc/
make: execvp: /etc/: Permission denied
make: *** [test] Error 127

上面报了一个Permission denied的错误,这是因为make尝试将/etc/当作可执行命令去执行了。

文件依赖

举个例子,有的makefile可能有类似下面的内容:

prog1: prog1.o utils.o
    cc -o prog1 prog1.o utils.o

这是在告诉make,目标文件prog1的生成依赖于prog1.o、utils.o。若依赖文件有缺失,会报错。

$<、$@、$^

$@:目标文件
$^:所有的依赖文件
$<:第一个依赖文件

下面举个例子:

新建一个Makefile文件,内容如下(注意要用tab进行缩进):

prog1: prog1.o utils.o
    echo $@
    echo $^
    echo $<

执行make:

$ make prog1 
make: *** No rule to make target `prog1.o', needed by `prog1'.  Stop.

为了快速解决这个报错,我们伪造两个.o格式的文件:

$ touch prog1.o
$ touch utils.o

现在运行make:

$ make prog1 
echo prog1
prog1
echo prog1.o utils.o
prog1.o utils.o
echo prog1.o
prog1.o

.PHONY

PHONY的含义是:假的、伪造的。用来说明哪些文件是假的,例如n的Makefile中目标文件install、uninstall就是假的。

默认操作

定义目标文件all即可。

例如Makefile内容如下:

prog1: prog1.o utils.o
    echo $@
    echo $^
    echo $<

all: prog1

运行结果:

$ make
echo prog1
prog1
echo prog1.o utils.o
prog1.o utils.o
echo prog1.o
prog1.o

参考资料

玩转Bash脚本:特殊变量
shell编程——if语句 if -z -n -f -eq -ne -lt
linux中shell变量$#,$@,$0,$1,$2的含义解释
Terminal control/Preserve screen
ANSI Escape Sequences: Colours and Cursor Movement
Bash Prompt HOWTO
shell中${ } 的一些特异功能

细碎的知识点

以下是我阅读n源码时候整理的细碎的知识点,与n的设计无关。

$#

作用:获取参数个数。
示例:

# !/usr/bin/env bash
echo $#

用例:

$ bash mytest.sh 
0
$ bash mytest.sh foo
1
$ bash mytest.sh foo bar
2

${var-value}

value作为默认值。如果$var是空值,那么使用value。

示例:

# !/usr/bin/env bash
N_PREFIX=${N_PREFIX-/usr/local}
echo $N_PREFIX

用例:

$ bash mytest.sh 
/usr/local
$ N_PREFIX=/usr bash mytest.sh 
/usr

$* 与 $@

通配符*将所有参数视作一个变量,而@则可以理解为所有参数的集合。

数组

linux shell 数组建立及使用技巧
Linux Shell数组和关联数组

示例:

# !/usr/bin/env bash
BINS=("node"
      "io")

echo $BINS          # 输出node
echo ${BINS[*]}     # 输出node io
echo ${BINS[0]}     # 输出node
echo ${BINS[1]}     # 输出io
echo ${#BINS[*]}    # 输出2, 数组长度

# 遍历,依次输出node,io
for var in ${BINS[*]}
do
    echo $var
done

运行结果:

$ bash mytest.sh 
node
node io
node
io
2
node
io

判断语句中的-n,-z

if [ -n $string ]:如果string 非空(非0),返回0(true)。
if [ -z $string ]:如果string 为空。

示例:

# !/usr/bin/env bash
BINS=("node"
      "io")

echo "PROJECT_NAME: $PROJECT_NAME"
echo "PROJECT_URL: $PROJECT_URL"

if [ -n "$PROJECT_NAME" ]; then     # 如果不为空
  BINS+=($PROJECT_NAME)             # 数组中追加元素
  if [ -z "$PROJECT_URL" ]; then    # 如果为空
    echo "Must specify PROJECT_URL when supplying PROJECT_NAME"
  fi
fi

echo ${BINS[*]}                      # 输出数组中所有元素

输出:

$ bash mytest.sh 
PROJECT_NAME: 
PROJECT_URL: 
node io

$ PROJECT_NAME="dev" bash mytest.sh 
PROJECT_NAME: dev
PROJECT_URL: 
Must specify PROJECT_URL when supplying PROJECT_NAME
node io dev

$ PROJECT_NAME="dev" PROJECT_URL="urlllll" bash mytest.sh 
PROJECT_NAME: dev
PROJECT_URL: urlllll
node io dev

curl命令的若干参数

-#: 用于显示进度条。

-L: 用于跳转。如果访问的网页响应3××,则去请求新的网址。

-s:--silent,即quiet。

wget命令的参数

--no-check-certificate: 不校验证书。
-q: quiet,关掉wget的输出。 -O: 指定获取的数据存放的位置。

下面的命令是将数据存放在baidu.html这个文件中:

$ wget http://www.baidu.com -O baidu.html
--2015-11-05 10:44:45--  http://www.baidu.com/
Resolving www.baidu.com (www.baidu.com)... 119.75.218.70, 119.75.217.109
Connecting to www.baidu.com (www.baidu.com)|119.75.218.70|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 761
Saving to: ‘baidu.html’
....

下面的命令是将数据输出到终端:

$ wget http://www.baidu.com -O-
--2015-11-05 10:45:58--  http://www.baidu.com/
Resolving www.baidu.com (www.baidu.com)... 119.75.218.70, 119.75.217.109
Connecting to www.baidu.com (www.baidu.com)|119.75.218.70|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 761
Saving to: ‘STDOUT’
....

去掉多余的信息,将数据输出到终端:

$ wget http://www.baidu.com -O- -q
<!DOCTYPE html><html><body><script type="text/javascript">var u='https://www.baidu.com/?tn=91055223_hao_pg&rsr=5',ua=navigator.userAgent.toLowerCase();if(u.indexOf('baidu.com')>0||u.indexOf('360.cn')>0||u.indexOf('hao123.com')>0){var cc = document.cookie.split(';');for(var i=0;i<cc.length;i++){var name = cc[i].split("=")[0];document.cookie = name + '=; path=/; domain=.baidu.com; expires=Thu, 01 Jan 1970 00:00:01 GMT;';}}if(ua.indexOf('applewebkit')>0){var h = document.createElement('a');h.rel = 'noreferrer';h.href = u;document.body.appendChild(h);var evt = document.createEvent("MouseEvents");evt.initEvent("click", true, true);h.dispatchEvent(evt);} else {document.write('<meta http-equiv="Refresh" Content="0; Url=' + u + '" >');}</script></body></html>

command命令

$ command -v wget
/usr/bin/wget
$ command -v wget >/dev/null
$ command -v wget && echo "has wget"
/usr/bin/wget
has wget
$ command -v wget >/dev/null && echo "has wget"
has wget

脚本的参数

示例:

# !/usr/bin/env bash
echo "脚本名:$0"
echo "参数个数:$#"
echo "所有参数:$@"
echo "第1个参数:$1"
echo "第2个参数:$2"

用例:

$ bash mytest.sh 
脚本名:mytest.sh
参数个数:0
所有参数:
第1个参数:
第2个参数:

$ bash mytest.sh --name=letian -q
脚本名:mytest.sh
参数个数:2
所有参数:--name=letian -q
第1个参数:--name=letian
第2个参数:-q

函数的参数

示例:

# !/usr/bin/env bash

function foo() {
    echo "脚本名:$0"
    echo "参数个数:$#"
    echo "所有参数:$@"
    echo "第1个参数:$1"
    echo "第2个参数:$2"
}

foo
echo # 添加空行
foo --name=letian -q

关键字function可以省略。

运行结果:

脚本名:mytest.sh
参数个数:0
所有参数:
第1个参数:
第2个参数:

脚本名:mytest.sh
参数个数:2
所有参数:--name=letian -q
第1个参数:--name=letian
第2个参数:-q

前一命令的退出码$?

编写mytest.sh:

# !/usr/bin/env bash
exit 1

测试:

$ bash mytest.sh
$ echo $?
1

0代表正常退出。

$$

$$是当前进程的pid。
示例:

$ echo $$
7662

tput命令

tput 命令行使用说明
Terminal control/Preserve screen

关于smcup、rmcup,可以在终端中依次输入下面的命令看看效果:

tput smcup    # Save the display
echo 'Hello'
tput rmcup    # Restore the display

将命令执行的结果放入一个变量中

有两种方法,一个是$(),另外一种是是用反引号```。 示例:

$ result=$(ls)
$ echo $result
baidu.html mytest.sh
$ result="$(ls)"
$ echo $result
baidu.html mytest.sh
$ result=`ls`
$ echo $result
baidu.html mytest.sh

node命令直接执行字符串中的nodejs代码

$ node -p "1+1;"
2
$ node -p "console.log(1+1);"
2
undefined

grep命令的-A、-B参数

关于函数中变量的取值范围

要函数中定义的变量只在函数中有效,需要使用local关键字。 示例:

# !/usr/bin/env bash
foo() {
    bar="hi"
    local var="hello"
}

foo
echo "$bar"     # 输出hi
echo "$var"     # 输出空字符

键盘中特殊键与字符的对应

上方向键与\033[A对应,下方向键与\033[B对应。更多见Input Translation

这些字符的长度为3:

$ char=$'\033[A'; echo ${#char}
3

for 循环

示例1:

# !/usr/bin/env bash
for subdir in bin lib include share; do
    echo $subdir
done

输出:

bin
lib
include
share

关于Pax

PaX - Wikipedia
MPROTECT

字符串中字符的替换

示例:

# !/usr/bin/env bash
version=4.2.2
echo ${version//./ }   # 将.替换为空格

version=v4.2.2
echo ${version//./--}  # 将.替换为--

运行结果:

4 2 2
v4--2--2

提取字符串中数字

示例:

# !/usr/bin/env bash
semver="004 2 2"
echo $semver | grep -o -E '[0-9]+'  # 分行输出004、2、1
echo $semver | grep -o -E '[0-9]+' | head -1  # 输出004
echo $semver | grep -o -E '[0-9]+'  | head -1 | sed -e 's/^0\+//'   # 输出4
echo $semver | awk '{print $2}'     # 输出2

grep的-o是指只输出匹配的部分,-E指扩展的正则表达式。
head -1是把第一行提取出来。
sed -e 's/^0\+//'是把最前面的0替换为空。

使用shift删除指定数量的参数

shift命令用于删除参数列表中的前若干个参数,默认是1,也可以指定数量。

示例:

# !/usr/bin/env bash
foo() {
    echo $@
    shift 
    echo $@
    shift 3
    echo $@
}

foo 1 2 3 4 5 6

运行结果:

1 2 3 4 5 6
2 3 4 5 6
5 6

字体颜色配置

我们在设置PS1变量的时候常常与颜色打交道。使用printf、echo命令时也可以设置字体颜色,例如:

033是八进制,十进制值是27,在ASCII表中代表特殊字符ESC (escape),转义的意思。36m对应的颜色是Cyan,,91m对应的颜色是red,0m的作用是重置。

资料:
Chapter 6. ANSI Escape Sequences: Colours and Cursor Movement
Color Bash Prompt - ArchLinux
[What does a bash sequence '\033999D' mean and where is it explained?
Ultimate GIT PS1 bash prompt

sort命令

-k参数的解释:

-k, --key=POS1[,POS2] start a key at POS1 (origin 1), end it at POS2(default end of line)

position是从1开始的。

示例1:

# !/usr/bin/env bash
cat <<EOF | sort -k 1 -t ,
4,23,12
3,26,1
4,4,16
5,4,4
5,0,4
EOF

运行结果:

3,26,1
4,23,12
4,4,16
5,0,4
5,4,4

示例2:

# !/usr/bin/env bash
cat <<EOF | sort -k 1,1 -t ,
4,23,12
3,26,1
4,4,16
5,4,4
5,0,4
EOF

运行结果:

3,26,1
4,23,12
4,4,16
5,0,4
5,4,4

从第2、3行可以看到,23<4,这是按照字符串来比较的。

示例3:

# !/usr/bin/env bash
cat <<EOF | sort -k 1,2n -t ,
4,23,12
3,26,1
4,4,16
5,4,4
5,0,4
EOF

1,2n是指将第1个和第2个字段放在一起,当作数字来排序,n就numeric sort。 所以运行结果是:

4,4,16
5,0,4
5,4,4
3,26,1
4,23,12

示例4:

# !/usr/bin/env bash
cat <<EOF | sort -k 1,1n -k 2,2n -t ,
4,23,12
3,26,1
4,4,16
5,4,4
5,0,4
EOF

运行结果:

3,26,1
4,4,16
4,23,12
5,0,4
5,4,4

资料:
sort命令
Linux中sort命令用法
Trying to sort on two fields, second then first
Sorting multiple keys with Unix sort


( 本文完 )