Cách đọc dòng đầu vào của người dùng theo từng dòng cho đến khi Ctrl + D và bao gồm dòng nơi Ctrl + D được nhập


8

Kịch bản lệnh này lấy dòng đầu vào của người dùng sau dòng và thực thi myfunctiontrên mỗi dòng

#!/bin/bash
SENTENCE=""

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

Để dừng đầu vào, người dùng phải nhấn [ENTER]và sau đó Ctrl+D.

Làm cách nào tôi có thể xây dựng lại tập lệnh của mình để kết thúc chỉ Ctrl+Dvà xử lý dòng Ctrl+Dđược nhấn.


Những gì được bao gồm trong "chức năng"? Tôi chạy ví dụ của bạn, tôi đã sử dụng một cách đơn giản function myfunction { echo "you pressed $1" ; }; và ngay khi tôi nhấn nút điều khiển D kết thúc vòng lặp echo $sentenceđược thực hiện và thoát khỏi tập lệnh.
George Vasiliou

Câu trả lời:


5

Để làm điều đó, bạn phải đọc từng ký tự, không phải từng dòng.

Tại sao? Shell rất có thể sử dụng hàm thư viện C tiêu chuẩn read() để đọc dữ liệu mà người dùng đang nhập và hàm đó trả về số byte thực sự đã đọc. Nếu nó trả về 0, điều đó có nghĩa là nó đã gặp EOF (xem read(2)hướng dẫn; man 2 read). Lưu ý rằng EOF không phải là một ký tự mà là một điều kiện, tức là điều kiện "không còn gì để đọc", phần cuối của tệp .

Ctrl+Dgửi một ký tự kết thúc truyền (mã ký tự EOT, ASCII 4, $'\04'in bash) đến trình điều khiển đầu cuối. Điều này có tác dụng gửi bất cứ thứ gì có để gửi đến read()cuộc gọi chờ của shell.

Khi bạn nhấn Ctrl+Dnửa chừng để nhập văn bản trên một dòng, bất cứ điều gì bạn đã gõ cho đến nay đều được gửi đến trình bao 1 . Điều này có nghĩa là nếu bạn nhập Ctrl+Dhai lần sau khi gõ một dòng nào đó trên dòng, thì cái đầu tiên sẽ gửi một số dữ liệu và cái thứ hai sẽ không gửi , và read()cuộc gọi sẽ trả về 0 và shell sẽ hiểu là EOF. Tương tự, nếu bạn nhấn Entertheo sau Ctrl+D, shell sẽ nhận EOF ngay lập tức vì không có bất kỳ dữ liệu nào để gửi.

Vậy làm thế nào để tránh phải gõ Ctrl+Dhai lần?

Như tôi đã nói, đọc các ký tự đơn. Khi bạn sử dụng lệnh tích hợp readshell, nó có thể có bộ đệm đầu vào và yêu cầu read()đọc tối đa nhiều ký tự từ luồng đầu vào (có thể là 16 kb hoặc hơn). Điều này có nghĩa là shell sẽ nhận được một loạt 16 kb đầu vào, theo sau là một đoạn có thể nhỏ hơn 16 kb, theo sau là byte không (EOF). Khi gặp phải kết thúc của đầu vào (hoặc một dòng mới hoặc một dấu phân cách được chỉ định), điều khiển được trả về tập lệnh.

Nếu bạn sử dụng read -n 1để đọc một ký tự đơn, shell sẽ sử dụng bộ đệm của một byte đơn trong lệnh gọi của nó read(), tức là nó sẽ ngồi trong một ký tự đọc chặt chẽ theo ký tự, trả lại quyền điều khiển cho tập lệnh shell sau mỗi ký tự.

Vấn đề duy nhất read -nlà nó đặt thiết bị đầu cuối thành "chế độ thô", có nghĩa là các ký tự được gửi như chúng không có bất kỳ sự giải thích nào. Ví dụ: nếu bạn nhấn Ctrl+D, bạn sẽ nhận được một ký tự EOT theo nghĩa đen trong chuỗi của mình. Vì vậy, chúng tôi phải kiểm tra cho điều đó. Điều này cũng có tác dụng phụ là người dùng sẽ không thể chỉnh sửa dòng trước khi gửi nó tới tập lệnh, ví dụ bằng cách nhấn Backspacehoặc bằng cách sử dụng Ctrl+W(để xóa từ trước đó) hoặc Ctrl+U(để xóa đến đầu dòng) .

Để rút ngắn một câu chuyện dài: Sau đây là vòng lặp cuối cùng mà bashtập lệnh của bạn cần thực hiện để đọc một dòng đầu vào, đồng thời cho phép người dùng làm gián đoạn đầu vào bất cứ lúc nào bằng cách nhấn 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

Không đi sâu vào chi tiết về điều này:

  • IFS=xóa IFSbiến. Không có điều này, chúng tôi sẽ không thể đọc được không gian. Tôi sử dụng read -Nthay vì read -n, nếu không chúng tôi sẽ không thể phát hiện dòng mới. Các -rtùy chọn để readcho phép chúng tôi để đọc backslashes đúng cách.

  • Câu caselệnh tác động lên từng ký tự đọc ( $ch). Nếu $'\04'phát hiện EOT ( ), nó đặt got_eotthành 1 và sau đó rơi vào breakcâu lệnh đưa nó ra khỏi vòng lặp bên trong. Nếu một dòng mới ( $'\n') được phát hiện, nó sẽ thoát ra khỏi vòng lặp bên trong. Nếu không, nó thêm ký tự vào cuối linebiến.

  • Sau vòng lặp, dòng được in ra đầu ra tiêu chuẩn. Đây sẽ là nơi bạn gọi tập lệnh hoặc hàm sử dụng "$line". Nếu chúng tôi đến đây bằng cách phát hiện EOT, chúng tôi sẽ thoát khỏi vòng lặp ngoài cùng.

1 Bạn có thể kiểm tra điều này bằng cách chạy cat >filetrong một thiết bị đầu cuối và trong một thiết bị đầu cuối tail -f filekhác, sau đó nhập một dòng một phần vào catvà nhấn Ctrl+Dđể xem điều gì xảy ra trong đầu ra của tail.


Đối với ksh93người dùng: Vòng lặp ở trên sẽ đọc ký tự trả về vận chuyển thay vì ký tự dòng mới ksh93, điều đó có nghĩa là bài kiểm tra $'\n'sẽ cần phải thay đổi thành bài kiểm tra $'\r'. Vỏ cũng sẽ hiển thị những cái này như ^M.

Để giải quyết vấn đề này:

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

# vòng lặp ở đây, với $ '\ n' được thay thế bằng $ '\ r'

stty "$ stty_satted"

Bạn cũng có thể muốn xuất một dòng mới ngay trước khi breaknhận được chính xác hành vi tương tự như trong bash.


1
Tuy nhiên, cần lưu ý rằng với cách tiếp cận đó, vì read -Nlàm cho thiết bị tty rời khỏi chế độ chính tắc , người dùng sẽ không thể sử dụng dấu gạch chéo ngược / Ctrl-W / Ctrl-U để chỉnh sửa văn bản hoặc sử dụng Ctrl-V để nhập đặc biệt nhân vật.
Stéphane Chazelas

@ StéphaneChazelas Cảm ơn. Vâng, tôi sẽ lưu ý điều này trong câu trả lời của tôi.
Kusalananda

1

Trong chế độ mặc định của thiết bị đầu cuối, read()cuộc gọi hệ thống (khi được gọi với bộ đệm đủ lớn) sẽ dẫn đến các dòng đầy đủ. Lần duy nhất khi dữ liệu đọc sẽ không kết thúc bằng ký tự dòng mới là khi bạn nhấn Ctrl-D.

Trong các thử nghiệm của tôi (trên Linux, FreeBSD và Solaris), một dòng duy nhất read()chỉ mang lại một dòng duy nhất ngay cả khi người dùng đã nhập nhiều hơn theo thời gian read()được gọi. Trường hợp duy nhất mà dữ liệu đọc có thể chứa nhiều hơn một dòng là khi người dùng nhập một dòng mới dưới dạng Ctrl+VCtrl+J(ký tự tiếp theo bằng chữ theo sau là một ký tự dòng mới (trái ngược với ký tự quay trở lại được chuyển đổi thành dòng mới khi bạn nhấn Enter) .

Các readvỏ BUILTIN tuy nhiên đọc một byte đầu vào tại một thời điểm cho đến khi nó thấy một ký tự xuống dòng hoặc cuối của tập tin. Phần cuối của tệp sẽ là khi read(0, buf, 1)trả về 0, điều này chỉ có thể xảy ra khi bạn nhấn Ctrl-Dvào một dòng trống.

Tại đây, bạn muốn đọc lớn và phát hiện Ctrl-Dkhi đầu vào không kết thúc bằng ký tự dòng mới.

Bạn không thể làm điều đó với readnội trang, nhưng bạn có thể làm điều đó với sysreadnội dung zsh.

Nếu bạn muốn tài khoản cho người dùng gõ ^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

Nếu bạn muốn xem xét foo^V^Jbarnhư một bản ghi (với một dòng mới được nhúng), giả sử mỗi read()bản ghi trả về một bản ghi:

#! /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

Ngoài ra, với zsh, bạn có thể sử dụng zshtrình chỉnh sửa dòng nâng cao của riêng mình để nhập dữ liệu và ánh xạ ^Dtới đó một tiện ích báo hiệu kết thúc đầu vào:

#! /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

Với bashhoặc các vỏ POSIX khác, tương đương với sysreadcách tiếp cận, bạn có thể thực hiện điều gì đó bằng cách sử dụng ddđể thực hiện các read()cuộc gọi hệ thống:

#! /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

Tôi luôn nghĩ zshphải chịu đựng "tính năng phình to", nhưng hóa ra một số tính năng dường như khá tiện dụng đôi khi. Giới thiệu về ddtrong shkịch bản: bạn có thể bình luận về lý do tại sao bạn đọc 8192byte (8KB)?
Kusalananda

1
@Kusalananda, 8192 là những gì zsh sysreadsử dụng theo mặc định (cũng là mức tối đa được hỗ trợ ở đó). Có gì đặc trưng của zshsao bạn không tìm thấy hữu ích? Lưu ý rằng zshmã cũng nằm trong các mô-đun động khác nhau, vì vậy sự phình to không ảnh hưởng đến nó nhiều như các shell khác như ksh93 hoặc bash.
Stéphane Chazelas

Cảm ơn. Không, chỉ là tôi thích những chiếc vỏ đơn giản. Có quá nhiều thứ đang diễn ra, ví dụ, sự hoàn thiện và những thứ "ma thuật" khác mà tôi không hiểu hết . Vì vậy, đó chỉ là một trường hợp sợ hãi những điều chưa biết, tôi cho rằng. Ý kiến ​​cá nhân, rõ ràng. Tham chiếu , Tham chiếu
Kusalananda

0

Tôi không rõ lắm về những gì bạn đang yêu cầu, nhưng nếu bạn muốn người dùng có thể nhập nhiều dòng và sau đó xử lý tất cả các dòng bạn có thể sử dụng mapfile. Nó nhận đầu vào của người dùng cho đến khi gặp EOF và sau đó trả về một mảng với mỗi dòng là một mục trong mảng.

SENTANCE=''
echo "Enter your input, press ctrl+D when finished"
mapfile input   #this takes user input until they terminate with ctrl+D 
for line in "${input[@]}
do 
    myfunction $line
done
echo $SENTANCE
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.