GNU Bash内置命令中的冒号(colon)有什么作用?

465

一个不做任何事情的命令,只是作为注释标记的存在,但实际上它本身是一个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

我想我真正想知道的是它可能具有什么历史应用。


我不会说返回特定值的命令“什么也不做”。除非函数式编程包括“什么也不做”。 :-) - LarsH
2
“null”程序的一个用途是注释Shell序列。这是根据肯·汤普森在他1976年的论文《Unix命令语言》中所述。https://github.com/susam/tucl/blame/master/the-unix-command-language.md#L218 - amdn
12个回答

545

历史上,Bourne shell 没有内置的 truefalse 命令。而是将 true 简单地别名为 :,将 false 别名为类似于 let 0 的东西。

: 在向古老的 Bourne 衍生 shell 移植方面略好于 true。例如,考虑既没有 ! pipeline 运算符,也没有 || list 运算符的情况(这在一些古老的 Bourne shell 中是存在的)。这将使得 if 语句的 else 子句成为基于退出状态进行分支的唯一手段:

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

由于if需要非空的then子句,而注释不被视为非空,因此:可作为无操作符。

现在(即在现代环境中),通常可以使用:true。它们都由POSIX指定,有些人认为true更易读。但是其中有一个有趣的区别::是所谓的POSIX 特殊内建命令,而true常规内建命令

  • 必须将特殊内建命令构建到shell中;常规内建命令只“通常”构建,但不能严格保证。在大多数系统的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 还明确指出,: 可能比 true 更快,尽管这显然是与具体实现相关的细节。


2
@OldPro:不,他说的true是一个常规内置命令是正确的,但他错误地认为exec使用的是/bin/true而不是内置命令。 - Dennis Williamson
2
@DennisWilliamson 我只是按照规范的措辞来理解。暗示当然是常规内置函数也应该有一个独立版本存在。 - ormaaj
36
+1 很棒的回答。我想指出一下初始化变量的用法,比如 : ${var?not initialized} 等等。 - tripleee
2
一个或多个不太相关的后续问题。你说 : 是一个特殊的内置符号,不应该有一个以它命名的函数。但是最常见的 fork bomb 的例子不是将函数命名为 : 吗?:(){:|:&};: - Chong
将此示例进行交叉链接:https://unix.stackexchange.com/questions/25945/how-to-check-if-there-are-no-parameters-provided-to-a-command/162421#162421 - Nate Anderson
显示剩余8条评论

95

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

#!/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的最简单方法之一。


40
afile更加简单,且能够达到相同的效果。 - earl
3
很棒,我会使用$vecho技巧来简化我正在维护的脚本。 - BarneySchmale
10
vecho=":"中引用冒号有什么好处?只是为了可读性吗? - leoj
确切地说。在我发现无法以这种方式使用“#”之后。 - gbarry

92

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

在这种情况下,你可以将参数扩展作为 :false 的参数,具体使用哪个取决于你需要退出状态码是 0 还是 1。一个例子可能是:

: "${var:=$1}"

由于 : 是一个内置函数,所以它应该非常快。


6
您可以使用它来处理算术扩展的副作用:: $((a += 1))(根据POSIX规范,++--运算符不需要实现)。在bash、ksh和可能其他的shell中,您也可以使用如下方法:((a += 1))((a++)),但这并没有被POSIX规范定义。 - pabouk - Ukraine stay strong
@pabouk 是的,这一切都是真的,尽管 (()) 被指定为一个可选功能。"如果以“(($)”开头的字符序列在前面加上“$”将被 shell 解析为算术扩展,实现了“((expression))”作为算术表达式进行求值的 shell 可能会将“((”视为引入算术求值而不是分组命令。" - ormaaj
请注意,可以使用任何扩展的副作用(即使它没有显式设置变量),因为它可以在下一行中通过 $_ 访问。因此,一系列 : 命令可以用于逐步转换一个值。 - pyrocrasty
当我在别人的脚本中看到:被用于这个目的时,我才了解它。这是一种应该被禁止的技巧,因为它太聪明了。 - Mark Ransom

68

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

:也可作为块注释使用(类似于C语言中的/* */)。例如,若欲跳过脚本中某段代码块,可采取以下方式:

: << 'SKIP'

your code block here

SKIP

6
不好的想法。在这里文档中的任何命令替换仍然会被处理。 - chepner
43
不是一个坏主意。您可以通过在定界符周围使用单引号来避免Here文档中的变量解析/替换::<<'SKIP' - Rondo
3
据我所知,您还可以使用“\”转义任何定界符字符以达到相同的效果::<<\SKIP - yyny
@zagpoint 这是Python从中获取其使用文档字符串作为多行注释的地方吗? - Sapphire_Brick
请注意,块注释 SKIP 的结束必须单独放在一行上。 - midnite

45

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

记录日志

以这个脚本为例:

set -x
: Logging message here
example_command

第一行set -x会让终端在运行命令之前将其打印输出,这是一个相当有用的结构。不过缺点是通常的echo 日志信息类型语句现在会打印两次信息。使用冒号方法可以解决这个问题。请注意,您仍然需要像为echo一样转义特殊字符。

Cron作业标题

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

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

这是一个 cron 任务,每天在 10:45 运行 /opt/backup.sh 脚本。使用这种技术的好处是当 /opt/backup.sh 输出一些内容时,可以使电子邮件主题看起来更美观。


默认的日志位置在哪里?我可以设置日志位置吗?这个目的更多是为了在脚本/后台进程期间在stdout中创建输出吗? - domdambrogia
4
@domdambrogia 当使用 set -x 时,打印出的命令(包括类似于 : foobar 的内容)会被发送到 stderr。 - Flimm

41

这类似于 Python 中的pass

其中一种用途是在编写函数之前创建占位符:

future_function () { :; }

38

如果你想将一个文件截断为0字节,比如用于清空日志,可以尝试以下方法:

:> file.log

24
> file.log 更简洁,并且达到相同的效果。 - amphetamachine
72
是的,但是那个开心的表情符号让我感觉很好 :> - Ahi Tuna
29
:cat /dev/null更加通用。一些shell(例如我的zsh)在没有给出命令的重定向时会自动实例化一个当前shell中的cat并监听stdin。使用:比使用cat /dev/null更简单。通常交互式shell和脚本的行为是不同的,但如果您编写的脚本也适用于交互式,通过复制和粘贴进行调试会更容易。 - Caleb
4
在现代shell中(假设“:”和“true”速度相同),“: > file”与“true > file”有什么不同(除了字符计数和笑脸)? 它们是相同的,都将一个空文件写入名为“file”的文件中,并且在成功时返回0。 - Adam Katz
假设 :true 的速度相同,它们是相同的。但这不是一个有效的假设。 - Tripp Kinetics
@TrippKinetics::> filetrue > file之间确实有区别。如果出现错误,:> file可能会导致shell中止。有关详细信息,请参见我的评论 - tom

30

你可以将反引号(``)与它一起使用,以执行一个命令而不显示其输出,就像这样:

: `some_command`

当然,你可以直接执行some_command > /dev/null,但: 版本要更加简短。

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


33
如果该命令将输出几兆字节的内容,则这种方式并不安全,因为Shell会缓冲输出,然后将其作为命令行参数(栈空间)传递给“:”。请注意保持原意,使文本更通俗易懂,不要添加解释。 - Juliano
2
顺便提一下,这引出了一个问题,有没有一种方法可以在不使用/dev/null的情况下丢弃管道的输出?假设/dev/null不存在。它毕竟可以从系统中删除... - Tripp Kinetics
2
@TrippKinetics 很难想象一个没有 /dev/null 的系统能够运行。不值得浪费时间去担心它。 - Mark Ransom
@MarkRansom 另外,很多东西不一定需要 /dev/null 才能正常运行。任何写入它的东西只会写入一个名为 /dev/null 的文件。 - Tripp Kinetics
写成 some_command | : 不就一样了吗?另外请注意,标准错误仍然可见。 - cipper
显示剩余2条评论

25

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

#!/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" "$@"
  • 只要在单词中没有发生参数扩展或别名,任何 shell 脚本中的单词都可以用引号包含而不改变其含义;这意味着 ':' 等价于 :(我们在此仅将其用引号括起来以实现下面描述的 JavaScript 语义)。

  • ... 正如上面所述,第一行的第一个命令是一个无操作(它相当于 : //,或如果您更喜欢引用这些单词,则为 ':' '//'。请注意,这里的 // 没有特殊的意义,就像在 JavaScript 中一样;它只是一个被丢弃的无意义单词)。

  • 最后,第一行的第二个命令(分号后面),是程序的真正核心:它是 exec 调用,用 Node.js 进程调用来评估脚本的其余部分,替换了正在调用的 shell 脚本。

  • 与此同时,在 JavaScript 中,第一行解析为字符串文字 (':'),然后是一个注释,该注释被删除;因此,对于 JavaScript 来说,该程序看起来是这样的:

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

    由于字符串字面值在单独一行上,因此它是一个无操作语句,因此会从程序中剥离。这意味着整行都被删除了,留下了您的程序代码(在此示例中为function(){ ... }函数体)。


    你好,你能解释一下 ://;~function(){} 的作用吗?谢谢 :) - Stphane
    2
    @Stphane 添加了一个分解!至于 ~function(){},那就有点复杂了。这里有一些其他的答案,虽然它们都没有很好地解释清楚…如果这些问题都不能满足您的需求,请在此发布一个新问题,我将很乐意深入回答。 - ELLIOTTCABLE
    2
    我没有注意到 node。所以函数部分都是关于 JavaScript 的!我对在 IIFE 前面使用一元运算符感到满意。起初我还以为这也是 Bash,实际上并没有真正理解你的帖子的含义。现在我没问题了,感谢你花时间添加“分解”! - Stphane
    ~{ No problem. (= } - ELLIOTTCABLE

    20

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

    假设你有一个库脚本mylib.sh,提供各种函数。你可以直接调用函数(. mylib.shlib_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并手动调用帮助函数。

    请注意(对于任何答案,不仅仅是这个,但它首先出现在我渲染的页面中),在冒号 : 后面必须有一个空格。也许这很明显,但对我来说并不是。 - undefined

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接