常见 Bash 内置变量介绍

目录

$0
$1, $2 等等
$#
$* 与 "$*"
$@ 与 "$@"
$!
$_
$$
$PPID
$?
$BASH
$BASH_VERSION
$EUID 与 $UID
$GROUPS
$HOME
$HOSTNAME
$IFS
$PATH
$OLDPWD
$PWD
$PS1
$PS2
$PS4

$0

执行 Bash 脚本时,Bash 会自动将脚本的名称保存在内置变量 $0 中。因为 $0 基于的是实际的脚本文件名称,而不是在脚本中进行硬编码,所以在重命名脚本文件的名称后,不需要修改脚本的内容。比如下面的脚本片段:

#!/bin/bash

ARGS=3 # 这个脚本需要 3 个参数.
E_BADARGS=65 # 传递给脚本的参数个数不对.
echo "Args number is : $#"
echo $0
if [ $# -ne "$ARGS" ]
# 测试脚本的参数个数。
then
    echo "Usage: $(basename $0) first-parameter second-parameter third-parameter"
    exit $E_BADARGS
fi
# 开始干正事儿

在上面的代码中我们使用了 $(basename $0) 的写法,这是因为 $0 会包含脚本文件的路径,为了让输出看起来清爽一些,我用 $(basename $0) 去掉了脚本的路径名称,下面是运行的结果:

$1, $2 等等

$0, $1,$2... 被称为位置参数。所谓的位置参数(positional parameter),指的是 Shell 脚本的命令行参数(argument);同时也表示在 Shell 函数内的函数参数。它们的名称是以单个的整数来命名。出于历史的原因,当这个整数大于 9 时,就应该以大括号{} 括起来。下面是一个简单的 demo:

#!/bin/bash
echo $1
echo $2
echo $3

$#

位置参数的个数,具体的用法请参考 $0 中的示例。

$* 与 "$*"

所有的位置参数。但是 $* 与 "$*" 的表现是不一样的,我们通过下面的 demo 来介绍其异同。
$* 提供分隔后的参数:

for arg in $*
do
    echo $arg
done

$* 和 $@ 的表现是一样的。

"$*" 把所有参数看作一个字符串:

for arg in "$*"
do
    echo $arg
done

$@ 与 "$@"

所有的位置参数。$@ 和 $* 的表现是一样的。
"$@" 能够提供看上去比较合理的结果:

for arg in "$@"
do
    echo $arg
done

下面是 "$@" 的一个比较常见的用法如下:

if[ "$1"='node' ]; then
    SCRIPT_FILE=
    for ARG in "$@"
    do
        if[ "${ARG}"='main.js' ]; then
            SCRIPT_FILE='main.js'
            break
        fi
    done
    if[ -z "$SCRIPT_FILE" ]; then
        exec "$@""main.js"
        exit 0;
    fi
fi
exec"$@"

这是在常见 nodejs 的 docker 镜像时经常使用的一段代码:

"$@" 还常常与 shift 命令一起使用来丢弃参数 $1
#!/bin/bash
# 使用./test.sh 1 2 3 4 5 来调用这个脚本
echo "$@" # 1 2 3 4 5
shift
echo "$@" # 2 3 4 5
shift
echo "$@" # 3 4 5
# 每次 "shift" 都会丢弃$1.
# "$@" 将包含剩下的参数. 

还可以使用 set 命令在脚本中设置位置参数:

#!/bin/bash

set -- "First one" "second" "third:one" "" "Fifth: :one"
# 设置这个脚本的参数, $1, $2, 等等.
index=1 # 起始计数.
echo "Listing args with \"\$@\":"
for arg in "$@"
do
    echo "Arg #$index = $arg"
    let "index+=1"
done # $@ 把每个参数都看成是单独的单词.
echo "Arg list seen as separate words."

$!

运行在后台的最后一个作业的 PID。

$ sleep 60 &
[1] 6238
$ echo "$!"
6238

如果有多个在后台运行的任务,就需要通过 $! 来获得 PID 并进行 wait:

$ sleep 60 &
$ pid1=$!
$ sleep 100 &
$ pid2=$!
$ wait $pid1      # 等待第一个后台进程结束
$ wait $pid2      # 等待第二个后台进程结束

$_

这个变量保存之前执行的命令的最后一个参数的值。
把下面的代码保存在 test.sh 文件中:

#!/bin/bash

echo $_ # ./test.sh

du >/dev/null # 这么做命令行上将没有输出.
echo $_ # du

ls -al >/dev/null # 这么做命令行上将没有输出.
echo $_ # -al (这是最后的参数)

:
echo $_ # :

下面是一个比较常见的用法,可以直接进入创建的目录:

$ mkdir hello && cd $_

$$

脚本自身的 PID (当前 bash 进程的 PID):

$PPID

进程的 $PPID 就是这个进程的父进程的 PID。

$?

$? 保存了最后所执行的命令的退出状态码,一般表示命令执行成功或失败。当函数返回之后,$? 保存函数中最后所执行的命令的退出状态码。这就是 bash 对函数 "返回值" 的处理方法。当一个脚本退出,$? 保存了脚本的退出状态码,这个退出状态码也就是脚本中最后一个执行命令的退出状态码。 0 表示成功,其它值表示错误。

当脚本以不带参数的 exit 命令来结束时,脚本的退出状态码就由脚本中最后执行的命令来决定(就是exit之前的命令)。不带参数的exit命令与 exit $? 的效果是一样的,甚至脚本的结尾不写 exit,也与前两者的效果相同。
我们还可以把 $? 保存到变量中,从而让脚本返回其中某个命令的返回值:

#!/bin/bash
set -x
go get -d -v golang.org/x/net/html
go get -u github.com/jstemmer/go-junit-report
go test -v 2>&1 > tmp
status=$?
$GOPATH/bin/go-junit-report < tmp > test_output.xml

exit ${status}

上面的程序把 go test 命令的返回值保存到了变量 status 中,并通过 exit ${status} 作为脚本的返回值。

关于退出状态
在 Linux 系统中,程序(包括脚本)的退出状态是非常有用的,只要程序执行完成,就会向 Shell 返回一个退出状态码。这个状态码是一个数值,指明了程序是否成功结束。按照惯例,退出状态码为 0 表示程序运行成功;非 0 表示程序运行失败,不同的值对应着不同的失败原因。
造成程序运行失败的原因可能是非法参数,也可能是出现了错误的条件。比如 cp 命令,退出状态码 1 表示文件没有找到,2 表示文件不可读,3 表示目标目录没有找到,4 表示目标目录不可写,5 表示一般性错误。

$BASH

Bash 的二进制程序文件的路径:

$BASH_VERSION

检查系统上安装的 Bash 版本号:

检查 $BASH_VERSION 对于判断系统上到底运行的是哪个 shell 来说是一种非常好的方法。变量 $SHELL有时候不能够给出正确的答案。

$EUID 与 $UID

$EUID 表示 "有效" 用户 ID。

$UID 表示 用户ID号,是当前用户的用户标识号, 记录在 /etc/passwd 文件中。这是当前用户的真实 id, 即使只是通过使用 su 命令来临时改变为另一个用户标识, 这个 id 也不会被改变。$UID 是一个只读变量,不能在命令行或者脚本中修改它。

$GROUPS

当前用户所属的组。
这是一个当前用户的组 id 数组, 与记录在 /etc/passwd 文件中的内容一样:

$HOME

用户的 home 目录,一般是 /home/username。

$HOSTNAME

主机名称。

$IFS

内部域分隔符。这个变量用来决定 Bash 在解释字符串时如何识别域,或者单词边界。
$IFS默认为空白(空格, 制表符,和换行符),但这是可以修改的,比如在分析逗号分隔的数据文件时,就可以设置为逗号。注意 $* 使用的是保存在 $IFS 中的第一个字符来分隔位置参数的。
$IFS 处理其他字符与处理空白字符不同的 demo:

#!/bin/bash

output_args_one_per_line()
{
    for arg
        do echo "[$arg]"
    done
}

echo "IFS=\" \""
echo "-------"

IFS=" "
var=" a b c "
output_args_one_per_line $var
echo; echo "IFS=:"
echo "-----"

IFS=:
var=":a::b:c:::" # 与上边一样, 但是用" "替换了":".
output_args_one_per_line $var
# 使用 : 后,冒号前后的空字符也被解析了。
exit 0

执行上面的脚本,结果如下:

$PATH

可执行文件的搜索路径。
当给出一个命令时,Bash 会自动生成一张哈希(hash)表,并且在这张哈希表中按照 PATH 变量中所列出的路径来搜索这个可执行命令。路径会存储在环境变量中,$PATH 变量本身就一个以冒号分隔的目录列表。通常情况下,系统都是在 /etc/profile 和 ~/.bashrc 中存储 $PATH 的定义,Ubuntu 是定义在 /etc/environment 文件中。

PATH=${PATH}:/opt/bin

将会把目录 /opt/bin 附加到当前目录列表中,在脚本中,这是一种把目录临时添加到 $PATH 中的权宜之计。当这个脚本退出时,$PATH 将会恢复以前的值(一个子进程,比如说一个脚本,是不能够修改父进程的环境变量的)。

当前的"工作目录",通常是不会出现在 $PATH 中的,这样做的目的是出于安全的考虑。因为当前目录是不断变化的,很有可能会存在与系统工具同名的恶意程序(比如你在网上下载了一个叫 cat 的恶意程序)。这时执行 cat 命令,就会运行当前目录下的 cat 恶意程序(把当前目录放在 PATH 变量的靠前位置的情况)。
还有一种情况,比如我们经常会自己用c语言或者其它的语言写一些程序,然后编译、链接为可执行文件。假如我们的可执行文件是做一些不可恢复性的操作,比如删除文件,格式化磁盘之类的。而这些文件名字又恰巧和我们系统 $PATH 下的某些常用可执行文件名字相同时,那么结果会出乎我们的意料。
也就是说当前目录是总在变化的,一会我们 cd 到这儿了,一会又 cd 到另一个地方去了。这样的话,当前目录下有哪些可执行文件也会随着改变的。有时候我们不会太在意自己处于的目录位置,如果当前目录在 $PATH中,那么我们也就不清楚自己干了什么。

而 $PATH 里面则放置了一些固定的目录,这些目录是不会变化的,这样的话,当我们输入命令时,永远可以保证不会随着自己的位置改变,而导致出乎意料。

$OLDPWD

前一个工作目录,可以通过下面的命令快速的回到前一个工作目录:

$ cd -

$PWD

工作目录(你当前所在的目录),这与内置命令 pwd 的作用相同:

下面的脚本演示了如何防止误删文件:

#!/bin/bash

E_WRONG_DIRECTORY=73
clear # 清屏.
TargetDirectory=/home/nick/testdir
cd $TargetDirectory
echo "Deleting stale files in $TargetDirectory."

if [ "$PWD" != "$TargetDirectory" ]
then # 防止偶然删错目录.
    echo "Wrong directory!"
    echo "In $PWD, rather than $TargetDirectory!"
    echo "Bailing out!"
    exit $E_WRONG_DIRECTORY
fi

rm -rf * # 删除文件
rm .[A-Za-z0-9]* # 删除点文件

echo "Done."
echo "Old files deleted in $TargetDirectory."
exit 0

执行上面的脚本,显示的结果如下:

$PS1

这是主提示符,可以在命令行中见到它,笔者的 Ubuntu16.04 中为:

看起来有些复杂,其实是添加了一些字体颜色的设置等内容。

$PS2

第二提示符,当你需要额外输入的时候,你就会看到它,默认值为 ">":

当我们往命令行上粘贴一个多行的命令时就会看到它的身影:

$PS4

第四提示符,当我们使用 -x 选项来调用脚本时,这个提示符会出现在每行输出的开头,默认为 "+":

运行下面的脚本:

set -x
echo "Hello nick"
echo 'This will show $PS4'

参考:
Bash Internal Variables
《高级 Bash 脚本编程指南》
《Unix/Linux/OS X 中的 Shell 编程》

posted @ 2018-11-10 09:29  sparkdev  阅读(8016)  评论(0编辑  收藏  举报