如何逐行讀取使用者輸入直到 Ctrl+D 並包含鍵入 Ctrl+D 的行

如何逐行讀取使用者輸入直到 Ctrl+D 並包含鍵入 Ctrl+D 的行

此腳本逐行取得使用者輸入,並myfunction在每一行執行

#!/bin/bash
SENTENCE=""

while read word
do
    myfunction $word"
done
echo $SENTENCE

若要停止輸入,使用者必須按[ENTER],然後按Ctrl+D

如何重建腳本以僅結束Ctrl+D並處理按下的行Ctrl+D

答案1

為此,您必須逐字符閱讀,而不是逐行閱讀。

為什麼? shell 很可能使用標準 C 函式庫函數read() 來讀取使用者輸入的數據,而該函數傳回實際讀取的位元組數。如果它返回零,則表示它遇到了 EOF(請參閱手冊read(2)man 2 read)。注意,EOF不是一個字符,而是一個條件,即條件“沒有更多內容可讀”,文件結尾

Ctrl+D發送一個傳輸結束字符 (EOT,ASCII 字元代碼 4,$'\04'bash) 到終端驅動程式。這具有發送任何要傳送到read()shell 的等待呼叫的效果。

當您Ctrl+D在一行中輸入文字時按到一半,到目前為止您輸入的任何內容都會發送到 shell 1。這意味著,如果您 Ctrl+D在一行中輸入內容後輸入兩次,則第一個將發送一些數據,第二個將發送沒有什麼,呼叫read()將返回零並且 shell 將其解釋為 EOF。同樣,如果您按Enter後跟Ctrl+D,shell 會立即收到 EOF,因為沒有任何資料要發送。

那麼如何避免輸入Ctrl+D兩次呢?

正如我所說,閱讀單個字元。當您使用readshell 內建命令時,它可能有一個輸入緩衝區,並要求read()從輸入流中讀取最多這麼多字元(可能是 16 kb 左右)。這意味著 shell 將獲得一堆 16 kb 的輸入區塊,後面跟著一個可能小於 16 kb 的區塊,最後是零位元組 (EOF)。一旦遇到輸入結束(或換行符,或指定的分隔符),控制權將返回腳本。

如果您用來read -n 1讀取單個字符,shell 將在其對 的調用中使用單個字節的緩衝區read(),即它將處於一個緊密循環中,逐個字符地讀取,並在每個字符之後將控制權返回給shell 腳本。

唯一的問題read -n是它將終端設置為“原始模式”,這意味著字元按原樣發送,沒有任何解釋。例如,如果您按Ctrl+D,您將在字串中得到一個文字 EOT 字元。所以我們必須檢查一下。這還有一個副作用,即用戶在將行提交給腳本之前將無法對其進行編輯,例如按Backspace,或使用Ctrl+W(刪除前一個單字)或Ctrl+U(刪除到行的開頭) 。

使長話短說:以下是 bash腳本讀取一行輸入所需執行的最終循環,同時允許使用者透過按 隨時中斷輸入 Ctrl+D

while true; do
    line=''

    while IFS= read -r -N 1 ch; do
        case "$ch" in
            $'\04') got_eot=1   ;&
            $'\n')  break       ;;
            *)      line="$line$ch" ;;
        esac
    done

    printf 'line: "%s"\n' "$line"

    if (( got_eot )); then
        break
    fi
done

無需對此進行太多詳細說明:

  • IFS=清除IFS變數。如果沒有這個,我們將無法讀取空格。我使用read -N代替read -n,否則我們將無法偵測換行符。這個-r選項read使我們能夠正確讀取反斜線。

  • case語句作用於每個讀取的字元 ( $ch)。如果$'\04'偵測到EOT ( ),它將設為got_eot1,然後執行將break其從內循環中取出的語句。如果偵測到換行符 ( $'\n'),它就會跳出內循環。否則,它將字元添加到line變數的末尾。

  • 循環之後,該行被印到標準輸出。這將是您調用使​​用"$line".如果我們透過偵測 EOT 到達這裡,我們就會退出最外層循環。

1cat >file您可以透過在一個終端機和另一個終端機中執行來測試這一點tail -f file,然後在 中輸入部分行 cat並按 來Ctrl+D查看 的輸出中會發生什麼tail


對於ksh93使用者:上面的循環將讀取 中的回車符而不是換行符ksh93,這意味著對 的測試$'\n'將需要更改為對 的測試$'\r'。 shell 也將這些顯示為^M.

要解決此問題:

stty_saved="$( stty -g )"
stty-echoctl

#循環到這裡,$'\n' 替換為 $'\r'

stty“$stty_saved”

您可能還想在 之前明確輸出換行符號以break獲得與 中完全相同的行為bash

答案2

在終端設備的預設模式下,read()系統呼叫(當使用足夠大的緩衝區呼叫時)將導致整行。讀取資料不會以換行符號結尾的唯一情況是當您按 時Ctrl-D

在我的測試中(在 Linux、FreeBSD 和 Solaris 上),即使使用者在呼叫read()時輸入了更多內容,單一也只能產生一行。read()讀取資料可能包含多行的唯一情況是當使用者輸入換行符時Ctrl+VCtrl+J(文字下一個字元後跟文字換行符(而不是按 時將回車符轉換為換行符Enter)) 。

然而,內建的shellread一次讀取一個字節,直到看到換行符號或檔案結尾。那文件結尾會是當read(0, buf, 1)返回 0 時,只有當您按Ctrl-D空行時才會發生。

在這裡,您需要進行大量讀取並檢測Ctrl-D輸入何時不以換行符號結尾。

您不能使用read內建函數來做到這一點,但您可以使用 的sysread內建函數來做到這一點zsh

如果您想考慮使用者輸入^V^J

#! /bin/zsh -
zmodload zsh/system # for sysread

myfunction() printf 'Got: <%s>\n' "$1"

lines=('')
while (($#lines)); do
  if (($#lines == 1)) && [[ $lines[1] == '' ]]; then
    sysread
    lines=("${(@f)REPLY}") # split on newline
    continue
  fi

  # pop one line
  line=$lines[1]
  lines[1]=()

  myfunction "$line"
done

如果您想將其foo^V^Jbar視為一筆記錄(帶有嵌入的換行符),則假設每個記錄read()傳回一筆記錄:

#! /bin/zsh -
zmodload zsh/system # for sysread

myfunction() printf 'Got: <%s>\n' "$1"

finished=false
while ! $finished && sysread line; do
  if [[ $line = *$'\n' ]]; then
    line=${line%?} # strip the newline
  else
    finished=true
  fi

  myfunction "$line"
done

或者,使用zsh,您可以使用zsh自己的高級行編輯器來輸入資料並將^D其映射到表示輸入結束的小部件:

#! /bin/zsh -
myfunction() printf 'Got: <%s>\n' "$1"

finished=false
finish() {
  finished=true
  zle .accept-line
}

zle -N finish
bindkey '^D' finish

while ! $finished && line= && vared line; do
  myfunction "$line"
done

使用bashPOSIX shell 或其他 POSIX shell,對於等效的sysread方法,您可以透過使用來dd執行read()系統呼叫:

#! /bin/sh -

sysread() {
  # add a . to preserve the trailing newlines
  REPLY=$(dd bs=8192 count=1 2> /dev/null; echo .)
  REPLY=${REPLY%?} # strip the .
  [ -n "$REPLY" ]
}

myfunction() { printf 'Got: <%s>\n' "$1"; }
nl='
'

finished=false
while ! "$finished" && sysread; do
  case $REPLY in
    (*"$nl") line=${REPLY%?};; # strip the newline
    (*) line=$REPLY finished=true
  esac

  myfunction "$line"
done

答案3

我不太清楚您的要求,但如果您希望用戶能夠輸入多行,然後將所有行作為一個整體處理,您可以使用mapfile.它接受使用者輸入,直到遇到 EOF,然後返回一個數組,其中每一行都是數組中的一個項目。

程式.sh

#!/bin/bash

myfunction () {
    echo "$@"
}

SENTANCE=''
echo "Enter your input, press ctrl+D when finished"
mapfile input   #this takes user input until they terminate with ctrl+D
n=0
for line in "${input[@]}"
do
    ((n++))
    SENTANCE+="\n$n\t$(myfunction $line)"
done
echo -e "$SENTANCE"

例子

$: bash PROGRAM.sh
Enter your input, press ctrl+D when finished
this is line 1
this is line 2
this is not line 10
# I pushed ctrl+d here

1   this is line 1
2   this is line 2
3   this is not line 10

相關內容