GNU Bash内置的:(冒号)的目的是什么?

一个什么都不做的命令,只不过是一个注释引导,但实际上是一个内置的shell,它的目的是什么?

它比每次调用在脚本中插入注释要慢40%左右,这可能取决于注释的大小。我认为唯一可能的原因是:

# poor man's delay function
for ((x=0;x<100000;++x)) ; do : ; done


# inserting comments into string of commands
command ; command ; : we need a comment in here for some reason ; command


# an alias for `true'
while : ; do command ; done

我想我真正想要的是它在历史上的应用。

168116 次浏览

从历史上看, Bourne shell没有truefalse作为内置命令。相反,true被简单地别名为:,而false则被别名为类似let 0的东西。

在可移植性方面,:略优于true。作为一个简单的例子,考虑既没有!管道操作符也没有||列表操作符(就像一些古老的Bourne shell的情况一样)。这使得if语句的else子句成为基于退出状态进行分支的唯一方法:

if command; then :; else ...; fi

因为if需要一个非空的then子句,而注释不是非空的,所以:作为一个no-op。

现在(即:在现代上下文中)通常可以使用:true。两者都由POSIX指定,有些人认为true更容易阅读。然而,有一个有趣的区别::是所谓的POSIX 特殊的内置,而true常规的内置

  • 外壳内需要内置特殊的内置组件;常规内置只是“典型地”内置,但并不能严格保证。在大多数系统的PATH中,通常不应该有一个名为:的具有true功能的常规程序。

  • 可能最关键的区别是,对于特殊的内置程序,内置程序设置的任何变量——即使是在简单的命令求值期间的环境中——在命令完成后仍然存在,如下所示使用ksh93:

    $ unset x; ( x=hi :; echo "$x" )
    hi
    $ ( x=hi true; echo "$x" )
    
    
    $
    

    请注意,Zsh忽略了这一要求,GNU Bash也是如此,除非在POSIX兼容模式下操作,但所有其他主要的“POSIX sh衍生”shell都遵守这一要求,包括dash、ksh93和mksh

  • 另一个区别是常规内置必须与exec兼容-在这里使用Bash演示:

    $ ( exec : )
    -bash: exec: :: not found
    $ ( exec true )
    $
    
  • POSIX also explicitly notes that : may be faster than true, though this is of course an implementation-specific detail.

你可以将它与反撇号(``)一起使用来执行命令而不显示其输出,如下所示:

: `some_command`

当然,你可以只做some_command > /dev/null,但__abc1版本有点短。

话虽如此,我不建议这样做,因为这会让人们感到困惑。它只是作为一个可能的用例出现在脑海中。

它类似于Python中的pass

一种用法是将一个函数存根,直到它被写入:

future_function () { :; }

我用它来轻松地启用/禁用变量命令:

#!/bin/bash
if [[ "$VERBOSE" == "" || "$VERBOSE" == "0" ]]; then
vecho=":"     # no "verbose echo"
else
vecho=echo    # enable "verbose echo"
fi


$vecho "Verbose echo is ON"

因此

$ ./vecho
$ VERBOSE=1 ./vecho
Verbose echo is ON

这有助于编写干净的脚本。这不能用'#'完成。

同时,

: >afile

是保证'afile'存在但长度为0的最简单方法之一。

:的一个有用的应用是,如果你只对使用参数展开的副作用感兴趣,而不是实际将它们的结果传递给命令。

在这种情况下,你可以使用参数展开作为:false的参数,这取决于你想要的退出状态是0还是1。一个例子是

: "${var:=$1}"

由于:是内置的,它应该相当快。

如果你想把一个文件截断为零字节,这对清除日志很有用,试试这个:

:> file.log

:也可以用于块注释(类似于C语言中的/* */)。例如,如果你想在脚本中跳过一段代码,你可以这样做:

: << 'SKIP'


your code block here


SKIP

其他答案中没有提到的两个用法:

日志记录

以这个示例脚本为例:

set -x
: Logging message here
example_command

第一行set -x使shell在运行命令之前打印出该命令。这是一个非常有用的结构。缺点是通常的echo Log message类型的语句现在打印两次消息。冒号方法避开了这个问题。注意,你仍然需要转义特殊字符,就像你对echo那样。

Cron职位名称

我看到它在cron作业中被使用,像这样:

45 10 * * * : Backup for database ; /opt/backup.sh

这是一个cron作业,每天在10:45运行脚本/opt/backup.sh。这种技术的优点是,当/opt/backup.sh打印一些输出时,它使电子邮件主题看起来更好。

它对多语言程序也很有用:

#!/usr/bin/env sh
':' //; exec "$(command -v node)" "$0" "$@"
~function(){ ... }

这现在既是一个可执行的shell脚本而且,也是一个JavaScript程序:这意味着./filename.jssh filename.jsnode filename.js都可以工作。

(这种用法确实有点奇怪,但还是很有效。)


请作一些说明:

  • shell脚本逐行计算;而exec命令在运行时,用生成的命令终止shell和替换进程。这意味着对于shell来说,程序看起来是这样的:

    #!/usr/bin/env sh
    ':' //; exec "$(command -v node)" "$0" "$@"
    
  • As long as no parameter expansion or aliasing is occurring in the word, any word in a shell-script can be wrapped in quotes without changing its' meaning; this means that ':' is equivalent to : (we've only wrapped it in quotes here to achieve the JavaScript semantics described below)

  • ... and as described above, the first command on the first line is a no-op (it translates to : //, or if you prefer to quote the words, ':' '//'. Notice that the // carries no special meaning here, as it does in JavaScript; it's just a meaningless word that's being thrown away.)

  • Finally, the second command on the first line (after the semicolon), is the real meat of the program: it's the exec call which replaces the shell-script being invoked, with a Node.js process invoked to evaluate the rest of the script.

  • Meanwhile, the first line, in JavaScript, parses as a string-literal (':'), and then a comment, which is deleted; thus, to JavaScript, the program looks like this:

    ':'
    ~function(){ ... }
    

    由于string-literal本身在一行上,它是一个no-op语句,因此从程序中剥离;这意味着整行都被删除,留下只有你的程序代码(在本例中是function(){ ... }主体)

自我记录的功能

你也可以使用:将文档嵌入到函数中。

假设你有一个库脚本mylib.sh,提供了各种函数。你可以源库(. mylib.sh)然后直接调用函数(lib_function1 arg1 arg2),或者避免混淆你的命名空间,用函数参数(mylib.sh lib_function1 arg1 arg2)调用库。

如果你也可以输入mylib.sh --help并获得可用函数及其用法的列表,而不必在帮助文本中手动维护函数列表,这不是很好吗?

#!/bin/bash


# all "public" functions must start with this prefix
LIB_PREFIX='lib_'


# "public" library functions
lib_function1() {
: This function does something complicated with two arguments.
:
: Parameters:
: '   arg1 - first argument ($1)'
: '   arg2 - second argument'
:
: Result:
: "   it's complicated"


# actual function code starts here
}


lib_function2() {
: Function documentation


# function code here
}


# help function
--help() {
echo MyLib v0.0.1
echo
echo Usage: mylib.sh [function_name [args]]
echo
echo Available functions:
declare -f | sed -n -e '/^'$LIB_PREFIX'/,/^}$/{/\(^'$LIB_PREFIX'\)\|\(^[ \t]*:\)/{
s/^\('$LIB_PREFIX'.*\) ()/\n=== \1 ===/;s/^[ \t]*: \?['\''"]\?/    /;s/['\''"]\?;\?$//;p}}'
}


# main code
if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
# the script was executed instead of sourced
# invoke requested function or display help
if [ "$(type -t - "$1" 2>/dev/null)" = function ]; then
"$@"
else
--help
fi
fi

关于代码的一些注释:

  1. 所有“public"函数具有相同的前缀。只有这些可以被用户调用,并在帮助文本中列出。
  2. 自记录特性依赖于前一点,并使用declare -f来枚举所有可用的函数,然后通过sed对它们进行筛选,以仅显示具有适当前缀的函数。
  3. 用单引号将文档括起来是一个好主意,以防止不必要的扩展和空格删除。在文本中使用撇号/引号时也需要小心。
  4. 你可以编写代码来内部化库前缀,即用户只需要输入mylib.sh function1,它就会在内部被转换为lib_function1。这是留给读者的练习。
  5. 帮助函数被命名为"——help"。这是一种方便(即惰性)的方法,它使用库调用机制来显示帮助本身,而不必为$1编写额外的检查代码。与此同时,如果您源化库,它将使您的名称空间变得混乱。如果你不喜欢这样,你可以将名称更改为lib_help,或者在主代码中检查--help的参数,并手动调用帮助函数。

我在一个脚本中看到了这种用法,并认为它可以很好地替代在脚本中调用basename。

oldIFS=$IFS
IFS=/
for basetool in $0 ; do : ; done
IFS=$oldIFS
< p >… 这是替换代码:basetool=$(basename $0)

另一种方法,这里还没有提到的是在无限while循环中初始化参数。下面不是最清晰的示例,但它达到了它的目的。

#!/usr/bin/env bash
[ "$1" ] && foo=0 && bar="baz"
while : "${foo=2}" "${bar:=qux}"; do
echo "$foo"
(( foo == 3 )) && echo "$bar" && break
(( foo=foo+1 ))
done