Làm thế nào để POSIX-ly đếm số lượng dòng trong một biến chuỗi?


10

Tôi biết tôi có thể làm điều này trong Bash:

wc -l <<< "${string_variable}"

Về cơ bản, tất cả mọi thứ tôi tìm thấy liên quan đến <<<toán tử Bash.

Nhưng trong vỏ POSIX, <<<không được xác định và tôi đã không thể tìm thấy một phương pháp thay thế trong nhiều giờ. Tôi khá chắc chắn rằng có một giải pháp đơn giản cho vấn đề này, nhưng thật không may, tôi đã không tìm thấy nó cho đến nay.

Câu trả lời:


11

Câu trả lời đơn giản wc -l <<< "${string_variable}"là một phím tắt ksh / bash / zsh cho printf "%s\n" "${string_variable}" | wc -l.

Thực tế có một số khác biệt trong cách thức hoạt động <<<và đường ống: <<<tạo một tệp tạm thời được truyền làm đầu vào cho lệnh, trong khi |tạo đường ống. Trong bash và pdksh / mksh (nhưng không phải trong ksh93 hoặc zsh), lệnh ở phía bên phải của đường ống chạy trong một khung con. Nhưng những khác biệt này không quan trọng trong trường hợp cụ thể này.

Lưu ý rằng về mặt đếm dòng, điều này giả định rằng biến không trống và không kết thúc bằng dòng mới. Không kết thúc bằng một dòng mới là trường hợp khi biến là kết quả của việc thay thế lệnh, vì vậy bạn sẽ nhận được kết quả đúng trong hầu hết các trường hợp, nhưng bạn sẽ nhận được 1 cho chuỗi trống.

Có hai điểm khác biệt giữa var=$(somecommand); wc -l <<<"$var"somecommand | wc -l: sử dụng thay thế lệnh và biến tạm thời loại bỏ các dòng trống ở cuối, quên đi liệu dòng đầu ra cuối cùng có kết thúc trong một dòng mới hay không (luôn luôn làm như vậy nếu lệnh xuất ra tệp văn bản không trống hợp lệ) và vượt qua một nếu đầu ra trống. Nếu bạn muốn giữ cả hai dòng kết quả và đếm, bạn có thể làm điều đó bằng cách nối thêm một số văn bản đã biết và tước nó ở cuối:

output=$(somecommand; echo .)
line_count=$(($(printf "%s\n" "$output" | wc -l) - 1))
printf "The exact output is:\n%s" "${output%.}"

1
@Inian Keeping wc -lhoàn toàn tương đương với bản gốc: <<<$foothêm một dòng mới vào giá trị của $foo(ngay cả khi $footrống). Tôi giải thích trong câu trả lời của mình tại sao điều này có thể không phải là điều muốn, nhưng đó là điều được hỏi.
Gilles 'SO- ngừng trở nên xấu xa'

2

Không tuân thủ hệ thống tích hợp sẵn, sử dụng các tiện ích bên ngoài như grepawkvới các tùy chọn tuân thủ POSIX,

string_variable="one
two
three
four"

Làm với grepđể bắt đầu dòng

printf '%s' "${string_variable}" | grep -c '^'
4

Và với awk

printf '%s' "${string_variable}" | awk 'BEGIN { count=0 } NF { count++ } END { print count }'

Lưu ý rằng một số công cụ GNU, đặc biệt, GNU grepkhông tôn trọng POSIXLY_CORRECT=1tùy chọn để chạy phiên bản POSIX của công cụ. Trong grephành vi duy nhất bị ảnh hưởng bởi việc đặt biến sẽ là sự khác biệt trong xử lý thứ tự của các cờ dòng lệnh. Từ tài liệu ( grephướng dẫn GNU ), có vẻ như

POSIXLY_CORRECT

Nếu được đặt, grep hoạt động như POSIX yêu cầu; mặt khác, grephoạt động giống như các chương trình GNU khác. POSIX yêu cầu các tùy chọn theo tên tệp phải được coi là tên tệp; theo mặc định, các tùy chọn như vậy được hoán vị ở phía trước danh sách toán hạng và được coi là tùy chọn.

Xem Cách sử dụng POSIXLY_CORRECT trong grep?


2
Chắc chắn wc -lvẫn còn khả thi ở đây?
Michael Homer

@MichaelHomer: Từ những gì tôi đã quan sát, wc -lcần một luồng phân tách dòng mới phù hợp (có dấu cuối '\ n` ở cuối để đếm đúng). Người ta không thể sử dụng một FIFO đơn giản để sử dụng với printf, ví dụ printf '%s' "${string_variable}" | wc -lcó thể không hoạt động như mong đợi nhưng <<<vì dấu vết được \nnối thêm bởi sự di truyền
Inian

1
Đó là những gì printf '%s\n'đã làm, trước khi bạn lấy nó ra ...
Michael Homer

1

Chuỗi ở đây <<<có khá nhiều phiên bản một dòng của tài liệu ở đây <<. Cái trước không phải là một tính năng tiêu chuẩn, nhưng cái sau là. Bạn có thể sử dụng <<quá trong trường hợp này. Chúng nên tương đương:

wc -l <<< "$somevar"

wc -l << EOF
$somevar
EOF

Mặc dù lưu ý rằng cả hai đều thêm một dòng mới vào cuối $somevar, ví dụ như bản in này 6, mặc dù biến chỉ có năm dòng:

s=$'foo\n\n\nbar\n\n'
wc -l <<< "$s"

Với printf, bạn có thể quyết định xem bạn có muốn thêm dòng mới hay không:

printf "%s\n" "$s" | wc -l         # 6
printf "%s"   "$s" | wc -l         # 5

Nhưng sau đó, lưu ý rằng wcchỉ tính các dòng hoàn chỉnh (hoặc số lượng ký tự dòng mới trong chuỗi). grep -c ^cũng nên đếm đoạn cuối cùng.

s='foo'
printf "%s" "$s" | wc -l           # 0 !

printf "%s" "$s" | grep -c ^       # 1

(Tất nhiên bạn cũng có thể đếm các dòng hoàn toàn trong trình bao bằng cách sử dụng phần ${var%...}mở rộng để loại bỏ từng dòng một trong vòng lặp ...)


0

Trong những trường hợp thường xuyên đáng ngạc nhiên khi những gì bạn thực sự cần làm là xử lý tất cả các dòng không trống bên trong một biến số (bao gồm cả đếm chúng), bạn có thể đặt IFS thành một dòng mới và sau đó sử dụng cơ chế tách từ của shell để phá vỡ Các dòng không trống cách nhau.

Ví dụ: đây là một hàm shell nhỏ có tổng các dòng không trống bên trong tất cả các đối số được cung cấp:

lines() (
IFS='
'
set -f #disable pathname expansion
set -- $*
echo $#
)

Dấu ngoặc đơn, thay vì dấu ngoặc nhọn, được sử dụng ở đây để tạo thành lệnh ghép cho thân hàm. Điều này làm cho hàm thực thi trong một lớp con để nó không gây ô nhiễm cho cài đặt mở rộng biến IFS và tên đường dẫn bên ngoài trên mỗi cuộc gọi.

Nếu bạn muốn lặp lại các dòng không trống, bạn có thể thực hiện tương tự:

IFS='
'
set -f
for line in $lines
do
    printf '[%s]\n' $line
done

Thao tác IFS theo cách này là một kỹ thuật thường bị bỏ qua, cũng thuận tiện để thực hiện những việc như phân tích tên đường dẫn có thể chứa khoảng trắng từ đầu vào cột được phân tách bằng tab. Tuy nhiên, bạn cần lưu ý rằng việc cố tình xóa ký tự khoảng trắng thường có trong cài đặt mặc định của tab không gian-tab-dòng mới có thể vô hiệu hóa việc tách từ ở những nơi bạn thường thấy.

Ví dụ: nếu bạn đang sử dụng các biến để xây dựng một dòng lệnh phức tạp cho một cái gì đó như ffmpeg, bạn có thể chỉ muốn bao gồm -vf scale=$scalekhi biến scaleđược đặt thành một thứ gì đó không trống. Thông thường bạn có thể đạt được điều này với ${scale:+-vf scale=$scale}nhưng nếu IFS không bao gồm ký tự không gian thông thường của nó tại thời điểm mở rộng tham số này, khoảng trống giữa -vfscale=sẽ không được sử dụng làm dấu tách từ và ffmpegsẽ được chuyển qua -vf scale=$scalenhư một đối số duy nhất, Điều đó sẽ không hiểu.

Để khắc phục điều đó, bạn cần phải đảm bảo IFS được đặt bình thường hơn trước khi thực hiện ${scale}mở rộng hoặc thực hiện hai lần mở rộng : ${scale:+-vf} ${scale:+scale=$scale}. Việc tách từ mà shell thực hiện trong quá trình phân tích cú pháp các dòng lệnh ban đầu, trái với việc phân tách nó thực hiện trong giai đoạn mở rộng xử lý các dòng lệnh đó, không phụ thuộc vào IFS.

Một cái gì đó khác có thể có giá trị trong khi bạn thực hiện loại điều này sẽ tạo ra hai biến vỏ toàn cầu để chỉ giữ một tab và chỉ là một dòng mới:

t=' '
n='
'

Bằng cách đó, bạn chỉ có thể bao gồm $t$nmở rộng nơi bạn cần các tab và dòng mới, thay vì xả rác tất cả mã của bạn với khoảng trắng được trích dẫn. Nếu bạn muốn tránh hoàn toàn khoảng trắng được trích dẫn trong vỏ POSIX không có cơ chế nào khác để làm như vậy, printfcó thể giúp bạn mặc dù bạn cần một chút lo lắng để giải quyết việc xóa các dòng mới trong phần mở rộng lệnh:

nt=$(printf '\n\t')
n=${nt%?}
t=${nt#?}

Đôi khi, thiết lập IFS như thể nó là một biến môi trường cho mỗi lệnh hoạt động tốt. Ví dụ: đây là một vòng lặp đọc tên đường dẫn được phép chứa khoảng trắng và hệ số tỷ lệ từ mỗi dòng của tệp đầu vào được phân định bằng tab:

while IFS=$t read -r path scale
do
    ffmpeg -i "$path" ${scale:+-vf scale=$scale} "${path%.*}.out.mkv"
done <recode-queue.txt

Trong trường hợp này, phần readdựng sẵn thấy IFS được đặt thành một tab, do đó, nó sẽ không phân chia dòng đầu vào mà nó đọc trên các khoảng trắng. Nhưng IFS=$t set -- $lines không hoạt động: shell mở rộng $lineskhi nó xây dựng các setđối số của buildin trước khi thực hiện lệnh, do đó, việc thiết lập IFS tạm thời theo cách chỉ áp dụng trong quá trình thực thi chính nội dung đã quá muộn. Đây là lý do tại sao các đoạn mã tôi đã đưa ra trên tất cả các IFS đặt ở một bước riêng biệt và tại sao chúng phải giải quyết vấn đề bảo tồn nó.

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.