Shell

Shell进阶脚本-内部变量

2021-11-21  本文已影响0人  Chris0Yang

内建变量

影响 Bash 脚本行为的变量

$BASH

Bash程序的路径

bash$ echo $BASH
/bin/bash

$BASH_ENV

这个环境变量会指向一个 Bash 启动文件,该文件在脚本被调用时会被读取

$BASH_SUBSHELL

该变量用于提示所处的 subshell 层级
这是在 Bash version 3 中被引入的新特性

$BASHPID

当前 Bash 进程实例的进程ID号
虽然与 $$ 变量不一样,但是通常它们会给出相同的结果

bash4$ echo $$
11015


bash4$ echo $BASHPID
11015


bash4$ ps ax | grep bash4
11015 pts/2    R      0:00 bash4
#!/bin/bash4

echo "\$\$ outside of subshell = $$"                              # 9602
echo "\$BASH_SUBSHELL  outside of subshell = $BASH_SUBSHELL"      # 0
echo "\$BASHPID outside of subshell = $BASHPID"                   # 9602

echo

( echo "\$\$ inside of subshell = $$"                             # 9602
  echo "\$BASH_SUBSHELL inside of subshell = $BASH_SUBSHELL"      # 1
  echo "\$BASHPID inside of subshell = $BASHPID" )                # 9603
  #  注意 $$ 总是返回父进程的 PID。

$BASH_VERSINFO[n]

这是一个6个元素的数组,其中包含了已经安装的 Bash 的版本信息。该变量与变量 $BASH_VERSION 类似,但是更加详细

# Bash 版本信息:

for n in 0 1 2 3 4 5
do
  echo "BASH_VERSINFO[$n] = ${BASH_VERSINFO[$n]}"
done

# BASH_VERSINFO[0] = 3                      # 主版本号
# BASH_VERSINFO[1] = 00                     # 次版本号
# BASH_VERSINFO[2] = 14                     # 补丁号
# BASH_VERSINFO[3] = 1                      # 构建版本号
# BASH_VERSINFO[4] = release                # 发行状态
# BASH_VERSINFO[5] = i386-redhat-linux-gnu  # 架构
                                            # (与 $MACHTYPE 相同)

$BASH_VERSION

已经安装的 Bash 的版本信息

bash$ echo $BASH_VERSION
3.2.25(1)-release
tcsh% echo $BASH_VERSION
BASH_VERSION: Undefined variable.

利用 $BASH_VERSION 来判断运行的是哪个 shell 是一个不错的方法,因为变量 $SHELL 并不总是能够给出正确的答案

$CDPATH

变量指定 cd 命令可以搜索的路径,路径之间用冒号进行分隔
该变量的功能类似于指定可执行文件搜索路径的变量 $PATH
可以在本地文件 ~/.bashrc 中设置该变量

bash$ cd bash-doc
bash: cd : bash-doc: No such file or directory


bash$ CDPATH=/usr/share/doc
bash$ cd bash-doc
/usr/share/doc/bash-doc


bash$ echo $PWD
/usr/share/doc/bash-doc

$DIRSTACK

指代目录栈中顶部的值,目录栈由命令 pushd 和 popd 控制
该变量相当于命令 dirs,但是 dirs 命令会显示整个目录栈

$EDITOR

脚本所调用的默认编辑器,通常是 vi 或是 emcas

$EUID

有效用户ID(EUID)是指当前用户正在使用的用户ID,可以通过 su 命令修改

$EUID 与 $UID 并不总是相同的

$FUNCNAME

当前运行函数的函数名

xyz23 ()
{
  echo "$FUNCNAME now executing."  # xyz2 now executing.
}

xyz23

echo "FUNCNAME = $FUNCNAME"        # FUNCNAME =
                                   # 如果在函数外则为空值。

$GLOBIGNORE

在文件匹配时所忽略的文件名模式列表。

$GROUPS

当前用户所属的用户组
该变量存储了当前用户所归属的用户组ID列表,是一个数组
内容与记录在文件 /etc/passwd 和文件 /etc/group 中的一致

root# echo $GROUPS
0


root# echo ${GROUPS[1]}
1


root# echo ${GROUPS[5]}
6

$HOME

当前用户的主目录,其值通常为 /home/username

$HOMENAME

系统启动的初始化脚本通过命令 hostname 给系统分配主机名
而函数 gethostname() 则是给 Bash 的内部变量 $HOSTNAME 赋值

$HOSTTYPE

主机类型
类似变量 $MACHTYPE,用于识别系统硬件信息

bash$ echo $HOSTTYPE
i686

$IFS

内部字段分隔符

该变量决定了 Bash 在解析字符串时如何去识别 字段 或单词边界

$IFS 的缺省值是空白符(空格,制表符以及换行符)
但其可以被修改,例如你在处理逗号分隔的文件时可以将其设置为逗号
需要注意 $* 使用保存在 $IFS 中的第一个字符

bash$ echo "$IFS"

(当 $IFS 设置为缺省值时,显示空行。)


bash$ echo "$IFS" | cat -vte
 ^I$
 $
(显示空白符:首先是一个空格,然后是 ^I [水平制表符],
 然后是换行符,最后在末尾显示 "$"。)


bash$ bash -c 'set w x y z; IFS=":-;"; echo "$*"'
w:x:y:z
(从字符串中解析命令,然后将命令参数分配给位置参数。)

通过设置 $IFS 来忽略文件路径名中空格带来的影响

IFS="$(printf '\n\t')"  

相比于其他字符,变量 $IFS 在处理空白符时有所不同

样例-1. $IFS 与空白符

#!/bin/bash
# ifs.sh


var1="a+b+c"
var2="d-e-f"
var3="g,h,i"

IFS=+
# 加号会被解析成分隔符。
echo $var1     # a b c
echo $var2     # d-e-f
echo $var3     # g,h,i

echo

IFS="-"
# 恢复对加号的默认解析。
# 现在减号会被解析成分隔符。
echo $var1     # a+b+c
echo $var2     # d e f
echo $var3     # g,h,i

echo

IFS=","
# 现在逗号会被解析成分隔符。
# 恢复对减号的默认解析。
echo $var1     # a+b+c
echo $var2     # d-e-f
echo $var3     # g h i

echo

IFS=" "
# 现在空格会被解析成分隔符。
# 逗号恢复成默认解析。
echo $var1     # a+b+c
echo $var2     # d-e-f
echo $var3     # g,h,i

# ======================================================== #

# 然而...
# $IFS 处理空白符的方式不同其他字符。

output_args_one_per_line()
{
  for arg
  do
    echo "[$arg]"
  done #  ^    ^   为了获得更好的视觉体验,把参数放到了括号里。
}

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

IFS=" "
var=" a  b c   "
#    ^ ^^   ^^^
output_args_one_per_line $var  # output_args_one_per_line `echo " a  b c   "`
# [a]
# [b]
# [c]


echo; echo "IFS=:"
echo "-----"

IFS=:
var=":a::b:c:::"               # 与上面一样的模式,
#    ^ ^^   ^^^                #+ 仅仅是将 " " 替换成了 ":" ...
output_args_one_per_line $var
# []
# [a]
# []
# [b]
# [c]
# []
# []

# 注意那些“空的”括号。
# 同样的情况也会出现在 awk 命令所使用的 "FS" 字段分隔符中。


echo

exit

$IGNOREEOF

忽略 EOF:用于指示 Shell 在注销前需要忽略多少个文件结束符(EOF,contrl-D)

$LC_COLLATE

经常会在文件 .bashrc 或是文件 /etc/profile 中被设置。该变量控制文件名扩展和模式匹配中的排序顺序
如果设置不得当,LC_COLLATE 将会导致 文件名匹配中出现非预期结果

在 Bash 2.05 版本之后,文件名匹配在不再区分中括号中字母的大小写
例如 ls [A-M]* 将会同时匹配 File1.txtfile1.txt 两个文件,如果想要恢复成之前的模式
则需要在文件 /etc/profile 或文件 ~/.bashrc 中通过语句 export LC_COLLATE=C 设置 LC_COLLATE 的值为 C

$LC_CTYPE

这个内部变量控制在 文件匹配 和模式匹配中的字符解析行为

$LINENO

该变量记录了其在脚本中被使用时所处行的行号。该变量只有在被使用时才有意义,在调试过程中非常有用

# *** 调试部分起始 ***
last_cmd_arg=$_  # 保存最后的命令。

echo "At line number $LINENO, variable \"v1\" = $v1"
echo "Last command argument processed = $last_cmd_arg"
# *** 调试部分终止 ***

$MACHTYPE

设备类型,是识别系统硬件

bash$ echo $MACHTYPE
i686

$OLDPWD

上一个工作目录(OLD-Print-Working-Directory),也就是之前所在的目录

$OSTYPE

操作系统类型

bash$ echo $OSTYPE
linux

$PATH

可执行文件搜索路径,其值通常包含 /usr/bin/usr/X11R6/bin//usr/local/bin 等路径

给定一个命令,shell就会自动从搜索路径包含的目录中利用哈希表搜索该可执行命令
而搜索路径就保存在环境变量 $PATH 中,其中包含的一系列目录则通过冒号进行分隔
通常情况下,$PATH 会定义在文件 /etc/profile 或文件 ~/.bashrc

bash$ echo $PATH
/bin:/usr/bin:/usr/local/bin/:/usr/X11R6/bin:/sbin:/usr/sbin
PATH=${PATH}:/opt/bin 表示添加目录 /opt/bin 到当前的搜索路径中
在脚本中可以通过这种方式临时添加目录到搜索路径
而当脚本结束时,$PATH 就会恢复到原始值
(类似于脚本这样的子进程所作出的修改,不会影响到。例如:  Shell 这样的父进程的环境)

基于安全考虑,通常在 $PATH 中会省略当前工作目录 ./

$PIPESTATUS

数组 变量保存了最后运行的前台 管道退出状态(es)

bash$ echo $PIPESTATUS
0

bash$ ls -al | bogus_command
bash: bogus_command: command not found
bash$ echo ${PIPESTATUS[1]}
127

bash$ ls -al | bogus_command
bash: bogus_command: command not found
bash$ echo $?
127
$PIPESTATUS 数组中的每一个元素都代表了该管道中相对应命令的退出状态
$PIPESTATUS[0] 表示管道中第一个命令的退出状态
$PIPESTATUS[1] 表示第二个命令的退出状态,以此类推

在Bash 3.0 以下版本的登录shell中,变量 $PIPESTATUS 可能会包含一个不正确的 0 值

tcsh% bash

bash$ who | grep nobody | sort
bash$ echo ${PIPESTATUS[*]}
0

如果脚本包含了上述代码,应该得到期望的输出是 0 1 0

在某些场景下,$PIPESTATUS 变量将会产生非预期结果

bash$ echo $BASH_VERSION
3.00.14(1)-release

bash$ ls | bogus_command | wc
bash: bogus_command: command not found
 0       0       0

bash$ echo ${PIPESTATUS[@]}
141 127 0

Chet Ramey 把上述非预期结果的原因归咎于 ls 命令的行为
如果 ls 将结果输出到没有被读取的管道上,产生的 SIGPIPE 信号将会终止 ls 命令,同时其 退出状态 从期望的 0 变为 141,而同样的情况也会发生在命令 tr

$PIPESTATUS 是一个易失的变量
该变量需要在目标管道执行完成后,且其他任何命令执行之前去捕获

bash$ ls | bogus_command | wc
bash: bogus_command: command not found
 0       0                0

bash$ echo ${PIPESTATUS[@]}
0 127 0

bash$ echo ${PIPESTATUS[@]}
0

$PIPESTATUS 不能给出所期望的信息的情况下,使用 pipeline 选项可能会有帮助

$PPID

一个进程的 $PPID 即该进程的父进程的进程ID(pid)
可以与命令 pidof 进行比较

$PROMPT_COMMAND

该变量存储在主提示符 $PS1 显示之前所需要执行的命令

$PS1

主提示符,即在命令行中显示的提示符

$PS2

次要提示符,当需要额外输入时出现的提示符。默认显示为 >

$PS3

三级提示符,显示在 select 循环中

$PS4

四级提示符,当使用 -x [verbose trace] 选项调用脚本时显示的提示符
默认显示为 +
其可以作为调试的辅助手段,把一些诊断信息显示在 $PS4 中可能会有帮助

P4='$(read time junk < /proc/$$/schedstat; echo "@@@ $time @@@ " )'
# 根据 Erik Brandsberg 提供的建议。
set -x
# 可以在后面写各种命令...

$PWD

工作目录(你当前所在的目录)
该变量是内建命令 pwd

#!/bin/bash

E_WRONG_DIRECTORY=85

clear # 清空屏幕。

TargetDirectory=/home/bozo/projects/GreatAmericanNovel

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]*    # 删除隐藏文件。
# rm -f .[^.]* ..?*   删除那些以多个点开头的文件。
# (shopt -s dotglob; rm -f *)   这样写也可以。

#  文件名可以包含ASCII码中范围为 0-255 的所有字符,
#+ 除了字符 "/"。
#  删除以一些特殊字符开头的文件,例如 -
#+ 留作练习。(提示: rm ./-weirdname 或者 rm -- -weirdname)
result=$?   # 删除操作的结果。如果删除成功,值为0。

echo
ls -al              # 是不是还有剩余没有删除的文件?
echo "Done."
echo "Old files deleted in $TargetDirectory."
echo

# 如果有其他需要,在这里完成。

exit $result

$REPLY

当没有给 read命令提供接收参数时的默认接收参数
该变量同样适用于 select菜单接收用户输入值的场景,需要注意的是用户只需要输入菜单项的编号,而不需要输入完整的菜单项内容

#!/bin/bash
# reply.sh

# REPLY 是 'read' 命令的默认接收参数。

echo
echo -n "What is your favorite vegetable? "
read

echo "Your favorite vegetable is $REPLY."
#  当且仅当 'read' 命令没有接收参数的时候,
#+ REPLY 才能保存最近一次 'read' 命令接收的值。

echo
echo -n "What is your favorite fruit? "
read fruit
echo "Your favorite fruit is $fruit."
echo "but..."
echo "Value of \$REPLY is still $REPLY."
#  因为变量 $fruit 接收了新一次 "read" 命令所读入的值,
#+ 所以 $REPLY 仍旧存储的是上一次接收的值。

echo

exit 0

$SECONDS

该变量记录到目前为止脚本执行的时间,单位为秒

#!/bin/bash

TIME_LIMIT=10
INTERVAL=1

echo
echo "Hit Control-C to exit before $TIME_LIMIT seconds."
echo

while [ "$SECONDS" -le "$TIME_LIMIT" ]
do   #   $SECONDS 是一个 shell 的内部变量。
  if [ "$SECONDS" -eq 1 ]
  then
    units=second
  else
    units=seconds
  fi

  echo "This script has been running $SECONDS $units."
  #  在一台性能较差或负载过重的设备上,
  #+ 这个脚本可能会偶尔跳过几个计数。
  sleep $INTERVAL
done

echo -e "\a"  # 发出蜂鸣声!

exit 0

$SHELLOPTS

该只读变量记录了 Shell 中已启用的 选项 列表

bash$ echo $SHELLOPTS
braceexpand:hashall:histexpand:monitor:history:interactive-comments:emacs

$SHLVL

当前 shell 的层级,即嵌套了多少层 Bash
如果命令行的层级 $SHLVL 为 1,那么在其中执行的脚本层级则增加到 2
该变量 不受 subshell 影响,当你需要指出嵌套了多少层 subshell 时,需要使用变量 $BASH_SUBSHELL

$TMOUT

如果 $TMOUT 被设为非 0 值 time,那么 shell 会在 $time 秒后超时,然后导致 Shell 登出
在 Bash 2.05b 版本之后,可以在脚本中将 read 命令与 $TMOUT 变量进行结合

# 只能在 Bash 2.05b 及之后的版本中成功执行。

TMOUT=3    # 提示会在 3 秒后超时。

echo "What is your favorite song?"
echo "Quickly now, you only have $TMOUT seconds to answer!"
read song

if [ -z "$song" ]
then
  song="(no answer)"
  # 默认值。
fi

echo "Your favorite song is $song."

在脚本中,同样也存在其他一些实现超时功能的更复杂的方法
其中一个方法是设置一个循环的计时器,当脚本超时的时候,计时器会给脚本发送一个信号
同时,也需要一个处理信号的程序来 捕获由循环计时器产生的中断

样例-2. 限时输入

#!/bin/bash
# timed-input.sh

# TMOUT=3    在新版本的 Bash 中起效。

TIMER_INTERRUPT=14
TIMELIMIT=3  # 在该实例中设置为 3 秒。
             # 同样可以设置成其他值。

PrintAnswer()
{
  if [ "$answer" = TIMEOUT ]
  then
    echo $answer
  else       # 不要混淆了这两个实例。
    echo "Your favorite veggie is $answer"
    kill $!  #  终止在后台运行的
             #+ 不再被需要的 TimerOn 函数。
             #  $! 代表最后一个在后台运行的作业的进程ID。
  fi

}


TimerOn()
{
  sleep $TIMELIMIT && kill -s 14 $$ &
  # 等待 3 秒,然后给脚本发送一个信号。
}


Int14Vector()
{
  answer="TIMEOUT"
  PrintAnswer
  exit $TIMER_INTERRUPT
}

trap Int14Vector $TIMER_INTERRUPT
# 我们的目的就是通过时间中断 (14) 终止程序。

echo "What is your favorite vegetable "
TimerOn
read answer
PrintAnswer


#  必须承认,这个实现限时输入的方法并不优雅。
#  但利用 "read" 命令的 "-t" 选项可以简化这个操作。
#  参考脚本 "t-out.sh"。
#  思考一下,如果不是对用户的单次输入时间进行限制,
#+ 而是对整个脚本的运行时间进行限制,应该怎么做?

#  如果你需要更优雅的写法 ...
#+ 可以考虑用 C 或者 C++ 来编写应用,
#+ 并使用其中包含的类似 'alarm' 或是 ‘setitimer' 等合适的库函数来实现计时。

exit 0

还有一种方法是使用 stty

样例-3. 再来一次,限时输入

#!/bin/bash
# timeout.sh

INTERVAL=5                # 超时所需的时间间隔

timedout_read() {
  timeout=$1
  varname=$2
  old_tty_settings=`stty -g`
  stty -icanon min 0 time ${timeout}0
  eval read $varname      # 或者直接写成 read $varname
  stty "$old_tty_settings"
  # 参考 "stty" 的帮助页面 (man)。
}

echo; echo -n "What's your name? Quick! "
timedout_read $INTERVAL your_name

#  该脚本也许并不能在所有类型的终端上正常运行。
#  最大的超时时间间隔依赖于终端。
#+ (通常是 25.5 秒)。

echo

if [ ! -z "$your_name" ]  # 如果在超时前输入了姓名 ...
then
  echo "Your name is $your_name."
else
  echo "Timed out."
fi

echo

# 该脚本的计时行为与 "timed-input.sh" 中的计时行为有所不同,
# 该脚本的计时器会在每次按键后被重置。

exit 0

可能最简单的方法就是利用 read命令的 -t 选项

样例-4. 限时 read

#!/bin/bash
# t-out.sh [time-out]
# 从 "syngin seven" 的建议中所汲取的灵感,谢谢你们。


TIMELIMIT=4         # 4 秒

read -t $TIMELIMIT variable <&1
#                           ^^^
#  在这个实例中,只有 Bash 1.x 或 Bash 2.x 版本需要 "<&1",
#  而在 Bash 3 及更高版本则不需要。

echo

if [ -z "$variable" ]  # 判断是否为空。
then
  echo "Timed out, variable still unset."
else
  echo "variable = $variable"
fi

exit 0

$UID

用户 ID
记录在文件 /etc/passwd中当前用户的用户标识号
该 ID 表示的是当前用户的真实 ID,即使用户通过 su 命令临时切换至另一个用户,这个 ID 也不会改变
$UID 是一个只读变量,不能够被命令行或是脚本中的命令所修改,并与内建命令 id]相对应

样例-5. 我是 root 用户吗?

#!/bin/bash
# am-i-root.sh:   我是否是 root 用户?

ROOT_UID=0   # Root 用户的 $UID 为 0。

if [ "$UID" -eq "$ROOT_UID" ]  # 只有真正的 "root" 用户才能经受得住考研。
then
  echo "You are root."
else
  echo "You are just an ordinary user (but mom loves you just the same)."
fi

exit 0


# ============================================================= #
# 下面的代码将不会被执行,因为脚本已经退出了。

# 另外一种判断是否是 root 用户的方法:

ROOTUSER_NAME=root

username=`id -nu`              # 或是...  username=`whoami`
if [ "$username" = "$ROOTUSER_NAME" ]
then
  echo "Rooty, toot, toot. You are root."
else
  echo "You are just a regular fella."
fi

变量 $ENV$LOGNAME$MAIL$TERM$USER 以及 $USERNAME 并不是 Bash 的 内建变量,而是在 Bash或系统的某个启动文件中,被设置而成的 环境变量
代表当前用户登录 shell 名称的变量 $SHELL 是在文件 /etc/password 或是某个初始化脚本中被设定的,它也不是一个 Bash 的内建变量

tcsh% echo $LOGNAME
bozo
tcsh% echo $SHELL
/bin/tcsh
tcsh% echo $TERM
rxvt

bash$ echo $LOGNAME
bozo
bash$ echo $SHELL
/bin/tcsh
bash$ echo $TERM
rxvt

位置参数

$0, $1, $2 等    
位置参数。出现在从命令行传递给脚本、函数或是通过内建命令 `set`]设置变量时

$#

命令行参数或是位置参数的个数

$*

将所有的位置参数整合,视作一个单词
该参数必须是被引用的状态,"$*"

$@

该参数等同于 $*,但其中每个参数都是独立的被引用的字符串
也就是说,所有的参数都是被原封不动的进行传递,并没有被解析或是扩展
这意味着,参数列表中的每一个参数都被独立视为一个单词
同样,该参数必须是被引用的状态$@

样例-6. 参数列表:利用 $*$@ 列出参数

#!/bin/bash
# arglist.sh
# 在调用该脚本时需要跟上一些参数,例如 "one two three" ...

E_BADARGS=85

if [ ! -n "$1" ]
then
  echo "Usage: `basename $0` argument1 argument2 etc."
  exit $E_BADARGS
fi

echo

index=1          # 初始化计数器。

echo "Listing args with \"\$*\":"
for arg in "$*"  # 如果这里没有引用 "$*",脚本将不会正常运行。
do
  echo "Arg #$index = $arg"
  let "index+=1"
done             # $* 将所有参数视作一个单词。
echo "Entire arg list seen as single word."

echo

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."

echo

index=1          # 重置计数器。

echo "Listing args with \$* (unquoted):"
for arg in $*
do
  echo "Arg #$index = $arg"
  let "index+=1"
done             # 未被引用的 $* 将所有参数视作独立的单词。
echo "Arg list seen as separate words."

exit 0

在 shift 命令执行后,@ 将会保留除了1 之外的剩余的命令行参数,而 $1 则会被丢弃

#!/bin/bash
# 使用 ./scriptname 1 2 3 4 5 调用脚本

echo "$@"    # 1 2 3 4 5
shift
echo "$@"    # 2 3 4 5
shift
echo "$@"    # 3 4 5

# 每一次 "shift" 都会丢弃参数 $1。
# "$@" 则包含了剩余的所有参数。

参数 $@ 也可被用作过滤 shell 脚本输入的工具。结构 cat $@ 可以接受来自标准输入 stdin 的输入,也可以接受传递给脚本的参数中的文件中的输入

根据分隔符 $IFS设置的不同,$*$@ 有时会出现不一致或非预期行为

样例-7. $*$@ 的不一致行为

#!/bin/bash

#  Bash 的内部变量 "$*" 和 "$@" 拥有不稳定的行为,
#+ 这些行为是否出现通常依赖于它们是否是被引用的状态。
#  下面的代码会演示在分词和换行时,这些变量所会出现的一些不一致的处理方式。


set -- "First one" "second" "third:one" "" "Fifth: :one"
# 设置脚本参数,$1, $2, $3 等等。

echo 

echo 'IFS unchanged, using "$*"'
c=0
for i in "$*"               # 被引用状态。
do echo "$((c+=1)): [$i]"   # 这一行在下面所有的例子中都保持不变。
                            # 输出参数。
done
echo ---

echo 'IFS unchanged, using $*'
c=0
for i in $*                 # 未被引用状态。
do echo "$((c+=1)): [$i]"
done
echo ---

echo 'IFS unchanged, using "$@"'
c=0
for i in "$@"
do echo "$((c+=1)): [$i]"
done
echo ---

echo 'IFS unchanged, using $@'
c=0
for i in $@
do echo "$((c+=1)): [$i]"
done
echo ---

IFS=:
echo 'IFS=":", using "$*"'
c=0
for i in "$*"
do echo "$((c+=1)): [$i]"
done
echo ---

echo 'IFS=":", using $*'
c=0
for i in $*
do echo "$((c+=1)): [$i]"
done
echo ---

var=$*
echo 'IFS=":", using "$var" (var=$*)'
c=0
for i in "$var"
do echo "$((c+=1)): [$i]"
done
echo ---

echo 'IFS=":", using $var (var=$*)'
c=0
for i in $var
do echo "$((c+=1)): [$i]"
done
echo ---

var="$*"
echo 'IFS=":", using $var (var="$*")'
c=0
for i in $var
do echo "$((c+=1)): [$i]"
done
echo ---

echo 'IFS=":", using "$var" (var="$*")'
c=0
for i in "$var"
do echo "$((c+=1)): [$i]"
done
echo ---

echo 'IFS=":", using "$@"'
c=0
for i in "$@"
do echo "$((c+=1)): [$i]"
done
echo ---

echo 'IFS=":", using $@'
c=0
for i in $@
do echo "$((c+=1)): [$i]"
done
echo ---

var=$@
echo 'IFS=":", using $var (var=$@)'
c=0
for i in $var
do echo "$((c+=1)): [$i]"
done
echo ---

echo 'IFS=":", using "$var" (var=$@)'
c=0
for i in "$var"
do echo "$((c+=1)): [$i]"
done
echo ---

var="$@"
echo 'IFS=":", using "$var" (var="$@")'
c=0
for i in "$var"
do echo "$((c+=1)): [$i]"
done
echo ---

echo 'IFS=":", using $var (var="$@")'
c=0
for i in $var
do echo "$((c+=1)): [$i]"
done

echo

# 尝试在 ksh 或是 zsh -y 下执行这个脚本。

exit 0

$@$* 仅在被双引号引用时才会表现出不同

样例-8. 当 $IFS 为空时 $*$@ 的表现

#!/bin/bash

#  如果 $IFS 被设置为空,
#+ 那么 "$*" 和 "$@" 将不会像期望的那样输出位置参数。

mecho ()       # 输出位置参数。
{
echo "$1,$2,$3";
}


IFS=""         # 设置为空。
set a b c      # 位置参数。

mecho "$*"     # abc,,
#                   ^^
mecho $*       # a,b,c

mecho $@       # a,b,c
mecho "$@"     # a,b,c

#  当 $IFS 为空时 $* 和 $@ 的行为
#+ 依赖于 Bash 或是 sh 所运行的版本。
#  因此不宜在脚本中使用这个“特性”。

exit

其他特殊参数

$-

使用 set命令设置的脚本标记

这个参数最开始是从 ksh 引入到 Bash中的。但很遗憾的是,该参数在 Bash 脚本中并不能可靠地运行
该参数可能的一个用法是用于 自检脚本是否可交互

$!

运行在后台的最后一个任务的 进程ID

LOG=$0.log

COMMAND1="sleep 100"

echo "Logging PIDs background commands for script: $0" >> "$LOG"
# 这样就可以监控命令,并在必要的时候终止它们。
echo >> "$LOG"

# 记录命令。

echo -n "PID of \"$COMMAND1\":  " >> "$LOG"
${COMMAND1} &
echo $! >> "$LOG"
# "sleep 100" 的 PID 是 1506

$! 用于控制任务:

possibly_hanging_job & { sleep ${TIMEOUT}; eval 'kill -9 $!' &> /dev/null; }
# 强制终止一个出错的程序。
# 非常有用,例如可以用在启动脚本中。

也可以这么使用:

TIMEOUT=30   # 以秒为单位的超时时间值。
count=0

possibly_hanging_job & {
        while ((count < TIMEOUT )); do
                eval '[ ! -d "/proc/$!" ] && ((count = TIMEOUT))'
                # 当前运行进程的详细信息都可以在 /proc 中找到。
                # "-d" 用于测试进程是否存在(即在 /proc 文件夹下该进程的文件夹是否存在)。
                # 我们在等待出问题的任务出现。
                ((count++))
                sleep 1
        done
        eval '[ -d "/proc/$!" ] && kill -15 $!'
        # 如果被挂起的任务正在运行就终止它。
}

#  -------------------------------------------------------------- #

#  然而,如果另外一个进程在 "hanging_job" 之后开始运行
#+ 该函数可能不能正常运行 ...
#  在那种情况下,一个非我们预期的任务会被终止。
#  Ariel Meragelman 提出了如下的解决方案。

TIMEOUT=30
count=0

possibly_hanging_job & {
    while ((count < TIMEOUT )); do
            eval '[ !-d "/proc/$lastjob" ] && ((count = TIMEOUT))'
            lastjob=$!
            ((count++))
            sleep 1
    done
    eval '[ -d "/proc/$lastjob" ] && kill -15 $lastjob'
}

exit

$_

该变量被设置为上一个执行的命令的最后一个参数
样例-9. 下划线变量

#!/bin/bash

echo $_              #  /bin/bash
                     #  仅通过调用 /bin/bash 执行该脚本。
                     #  注意这个结果会根据脚本如何被调用
                     #+ 而有所不同。

du >/dev/null        #  这样命令就不会在命令行上有任何输出。
echo $_              #  du

ls -al >/dev/null    #  这样命令就不会在命令行上有任何输出。
echo $_              #  -al (最后一个参数)

:
echo $_              #  :

$?

命令、函数或是脚本自身的 退出状态

$$

脚本自身的进程 ID
该变量 $$ 通常在脚本构建独有的临时文件时被使用,该方法通常比调用 mktemp 命令更简单

注记

上一篇下一篇

猜你喜欢

热点阅读