
此腳本逐行取得使用者輸入,並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兩次呢?
正如我所說,閱讀單個字元。當您使用read
shell 內建命令時,它可能有一個輸入緩衝區,並要求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_eot
1,然後執行將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
使用bash
POSIX 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