如何创建一个可以从标准输入中读取的Bash函数?

46

我有一些带参数的脚本,它们可以很好地工作,但我希望它们能够从标准输入中读取,例如从管道读取。例如,假设这个脚本叫做“read”:

#!/bin/bash
function read()
{
 echo $*
}

read $*

现在这个代码可以使用read "foo" "bar"来实现,但我想把它改成:

echo "foo" | read

我该如何完成这个任务?


4
需要将函数命名为'read'吗?有一个同名的Bash内置功能。 - jwfearn
对于命令行,请查看:如何在bash中从文件或stdin读取? - kenorb
7个回答

45

编写一个函数,使其可以读取标准输入,但在没有标准输入时也能正常工作有些棘手。如果您尝试从标准输入读取,它将一直阻塞,直到收到任何输入,就像在提示符下简单地键入cat一样。

在bash 4中,您可以通过使用-t选项和参数0来使用read解决此问题。如果有任何输入可用,则成功,但不会消耗任何输入;否则,失败。

这是一个简单的函数,如果它有来自标准输入的任何内容,则像cat一样工作,否则像echo一样。

catecho () {
    if read -t 0; then
        cat
    else
        echo "$*"
    fi
}

$ catecho command line arguments
command line arguments
$ echo "foo bar" | catecho
foo bar

这将使标准输入优先于命令行参数,即 echo foo | catecho bar 将输出 foo。为了使参数优先于标准输入 (echo foo | catecho bar 输出 bar),您可以使用更简单的函数。

catecho () {
    if [ $# -eq 0 ]; then
        cat
    else
        echo "$*"
    fi
}

该方法还有一个优点,就是可以与任何符合POSIX标准的shell一起使用,而不仅仅是某些版本的bash


2
在Bash 3中如何做到这一点?有什么建议吗? - jwfearn
2
不是很简单。我认为你需要尝试在后台从标准输入读取,然后在一秒钟后杀死它(如果它还没有完成),然后尝试确定它是否读取了任何内容。我不确定如何以一种万无一失的方式做到这一点(存在子shell中的变量问题,处理标准输入的部分读取以及竞争条件等问题)。 - chepner
@chepner:这个适用于哪个版本的bash?它在4.1.0上不起作用。 - DocSalvager
5
我已经成功在我的系统上运行了这个命令:' if read -t 0.00001 STDIN; then echo "$STDIN" '。最小的超时时间似乎至少取决于 CPU 速度,并且可能也与系统负载有关。像 0.01 或 0.001 这样的值可能是相当可靠的。 - DocSalvager
它无法与 sleep 10 | catecho 一起使用。有没有办法实现这个? - hek2mgl
显示剩余3条评论

39
你可以使用<<<来实现这种行为。read <<< echo "text"应该就可以了。
readly测试(我更喜欢不使用保留字):
function readly()
{
 echo $*
 echo "this was a test"
}

$ readly <<< echo "hello"
hello
this was a test
根据“Bash脚本,从标准输入流中读取值”的此答案,使用管道:
$ echo "hello bye" | { read a; echo $a;  echo "this was a test"; }
hello bye
this was a test

5
在第一个例子中,你实际上只是将单词“echo”输入到readly的标准输入中,而它会忽略标准输入,并将第一个参数设置为“hello”。你可以将“echo”更改为任何其他单词,结果都会相同。 - chepner
8
第二个示例将丢弃其标准输入的第一行之后的所有内容。 - chepner

15
把其他答案的内容结合起来,我找到了适合我的方法(下面这个虚构的例子是把小写输入变成大写):
  uppercase() {
    local COMMAND='tr [:lower:] [:upper:]'
    if [ -t 0 ]; then
      if [ $# -gt 0 ]; then
        echo "$*" | ${COMMAND}
      fi
    else
      cat - | ${COMMAND} 
    fi
  }

以下是一些示例(第一个没有输入,因此没有输出):

:; uppercase
:; uppercase test
TEST
:; echo test | uppercase 
TEST
:; uppercase <<< test
TEST
:; uppercase < <(echo test)
TEST

逐步操作:

  • test if file descriptor 0 (/dev/stdin) was opened by a terminal

    if [ -t 0 ]; then
    
  • tests for CLI invocation arguments

    if [ $# -gt 0 ]; then
    
  • echo all CLI arguments to command

    echo "$*" | ${COMMAND}
    
  • else if stdin is piped (i.e. not terminal input), output stdin to command (cat - and cat are shorthand for cat /dev/stdin)

    else
      cat - | ${COMMAND}
    

2
在某些情况下(例如从git-hook中调用脚本),[ -t 0 ] 对我来说失败了。我将其替换为 [ ! -p /dev/stdin ] 并且能够在更广泛的情况下使其工作。 - Eliran Malka

6

这是一个在bash中使用printf和标准输入实现sprintf函数的示例:

sprintf() { local stdin; read -d '' -u 0 stdin; printf "$@" "$stdin"; }

示例用法:

$ echo bar | sprintf "foo %s"
foo bar

这将让您了解函数如何从标准输入中读取的想法。

5

有点晚了,参考@andy回答后,这是我如何定义我的to_uppercase函数。

  • 如果标准输入不为空,则使用标准输入
  • 如果标准输入为空,则使用参数
  • 如果没有参数,则不执行任何操作
to_uppercase() {
  local input="$([[ -p /dev/stdin ]] && cat - || echo "$@")"
  [[ -n "$input" ]] && echo "$input" | tr '[:lower:]' '[:upper:]' 
}

用途:

$ to_uppercase 
$ to_uppercase abc
ABC
$ echo abc | to_uppercase 
ABC
$ to_uppercase <<< echo abc 
ABC

Bash版本信息:

$ bash --version
GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin17)

0

我发现可以使用testawk在一行中完成这个操作...

    test -p /dev/stdin  && awk '{print}' /dev/stdin

test -p 命令用于测试管道输入,它通过标准输入接受输入。只有在存在输入时,我们才想运行 awk 命令,否则它将无限期地等待永远不会到来的输入。

我已经将其放入函数中,以便于使用...

inputStdin () {
  test -p /dev/stdin  && awk '{print}' /dev/stdin  && return 0
  ### accepts input if any but does not hang waiting for input
  #
  return 1
}

使用方法...

_stdin="$(inputStdin)"

另一个函数使用awk而不是测试来等待命令行输入...
inputCli () {
  local _input=""
  local _prompt="$1"
  #
  [[ "$_prompt" ]]  && { printf "%s" "$_prompt" > /dev/tty; }
  ### no prompt at all if none supplied
  #
  _input="$(awk 'BEGIN {getline INPUT < "/dev/tty"; print INPUT}')"
  ### accept input (used in place of 'read')
  ###   put in a BEGIN section so will only accept 1 line and exit on ENTER
  ###   WAITS INDEFINITELY FOR INPUT
  #
  [[ "$_input" ]]  && { printf "%s" "$_input"; return 0; }
  #
  return 1
}

使用方法...

_userinput="$(inputCli "Prompt string: ")"

请注意,在第一个printf中的> /dev/tty似乎是必要的,以便在使用命令替换$(...)调用函数时打印提示符。
这种使用awk允许消除从键盘或stdin收集输入的古怪read命令。

0

另一个版本:

  1. 通过管道或参数传递文本进行操作
  2. 通过更改最后一行的命令,易于复制和粘贴
  3. 在bash、zsh中均可使用
# Prints a text in a decorated ballon
function balloon()
{
  (test -p /dev/stdin && cat - || echo $@) figlet -t | cowsay -n -f eyes | toilet -t --gay -f term
}

使用方法:

# Using with a pipe
$ fortune -s | balloon
# Passing text as parameter
balloon "$(fortune -s )"

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