shell: tiếp tục theo dõi dòng mới ('\ n') trong lệnh thay thế


14

Tôi muốn có thể nắm bắt đầu ra chính xác của một sự thay thế lệnh, bao gồm cả các ký tự dòng mới .

Tôi nhận ra rằng chúng bị tước theo mặc định, vì vậy một số thao tác có thể được yêu cầu để giữ chúng và tôi muốn giữ mã thoát gốc .

Ví dụ: được cung cấp một lệnh có số lượng dòng mới và mã thoát:

f(){ for i in $(seq "$((RANDOM % 3))"); do echo; done; return $((RANDOM % 256));}
export -f f

Tôi muốn chạy một cái gì đó như:

exact_output f

Và có đầu ra là:

Output: $'\n\n'
Exit: 5

Tôi quan tâm đến cả bashvà POSIX sh.


1
Dòng mới là một phần của $IFS, vì vậy nó sẽ không được ghi lại làm đối số.
Deathgrip

4
@Deathgrip Nó không có gì để làm với IFS(thử ( IFS=:; subst=$(printf 'x\n\n\n'); printf '%s' "$subst" )Chỉ dòng mới được rút gọn.. \tVà '' không, và IFSkhông ảnh hưởng đến nó.
PSkocik



Câu trả lời:


17

Vỏ POSIX

Thủ thuật thông thường ( 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ) để có được thiết bị xuất chuẩn hoàn chỉnh của lệnh là phải làm:

output=$(cmd; ret=$?; echo .; exit "$ret")
ret=$?
output=${output%.}

Ý tưởng là thêm và thêm .\n. Lệnh thay thế sẽ chỉ dải đó \n . Và bạn lột đồ .với ${output%.}.

Lưu ý rằng trong các shell khác zsh, nó vẫn sẽ không hoạt động nếu đầu ra có byte NUL. Với yash, điều đó sẽ không hoạt động nếu đầu ra không phải là văn bản.

Cũng lưu ý rằng ở một số địa phương, vấn đề là bạn sử dụng ký tự nào để chèn vào cuối. .nhìn chung sẽ ổn, nhưng một số khác có thể không. Ví dụ x(như được sử dụng trong một số câu trả lời khác) hoặc @sẽ không hoạt động tại một địa điểm sử dụng bảng mã BIG5, GB18030 hoặc BIG5HKSCS. Trong các bộ ký tự đó, mã hóa của một số ký tự kết thúc bằng cùng một byte với mã hóa của xhoặc @(0x78, 0x40)

Chẳng hạn, ūtrong BIG5HKSCS là 0x88 0x78 (và xlà 0x78 như trong ASCII, tất cả các bộ ký tự trên hệ thống phải có cùng mã hóa cho tất cả các ký tự của bộ ký tự di động bao gồm các chữ cái tiếng Anh @.). Vì vậy, nếu cmdđã printf '\x88'và chúng tôi chèn vào xsau nó, ${output%x}sẽ không thể loại bỏ nó xnhư $outputthực sự có chứa ū.

Sử dụng .thay thế có thể dẫn đến cùng một vấn đề về lý thuyết nếu có bất kỳ ký tự nào có mã hóa kết thúc ở cùng mã hóa như vậy ., nhưng vì đã kiểm tra một thời gian trước đây, tôi có thể nói rằng không có bảng mã nào có thể được sử dụng trong ngôn ngữ hệ thống Debian, FreeBSD hoặc Solaris có các ký tự như vậy đủ tốt cho tôi (và tại sao tôi đã giải quyết .đó cũng là biểu tượng để đánh dấu kết thúc câu bằng tiếng Anh nên có vẻ phù hợp).

Một cách tiếp cận đúng hơn như thảo luận của @Arrow sẽ là thay đổi ngôn ngữ thành C chỉ để tước ký tự cuối cùng ( ${output%.}) sẽ đảm bảo chỉ có một byte bị tước, nhưng điều đó sẽ làm phức tạp đáng kể mã và có khả năng gây ra sự cố tương thích của riêng nó.

lựa chọn thay thế bash / zsh

Với bashzsh, giả sử đầu ra không có NUL, bạn cũng có thể thực hiện:

IFS= read -rd '' output < <(cmd)

Để có được trạng thái thoát của cmd, bạn có thể làm wait "$!"; ret=$?trong bashnhưng không phải trong zsh.

RC / es / akanaga

Để đầy đủ, lưu ý rằng rc/ es/ akangacó một toán tử cho điều đó. Trong đó, thay thế lệnh, được biểu thị bằng `cmd(hoặc `{cmd}cho các lệnh phức tạp hơn) trả về một danh sách (bằng cách tách trên $ifs, dấu cách-tab-newline theo mặc định). Trong các shell đó (trái ngược với các shell giống như Bourne), việc tước dòng mới chỉ được thực hiện như một phần của quá trình $ifsphân tách đó . Vì vậy, bạn có thể để trống $ifshoặc sử dụng ``(seps){cmd}biểu mẫu nơi bạn chỉ định dấu phân cách:

ifs = ''; output = `cmd

hoặc là:

output = ``()cmd

Trong mọi trường hợp, trạng thái thoát của lệnh bị mất. Bạn cần phải nhúng nó vào đầu ra và trích xuất nó sau đó sẽ trở nên xấu xí.

Trong cá, thay thế lệnh là có (cmd)và không liên quan đến một lớp con.

set var (cmd)

Tạo một $varmảng với tất cả các dòng trong đầu ra của cmdif $IFSkhông trống hoặc với đầu ra cmdbị tước tới một (trái ngược với tất cả trong hầu hết các shell khác) ký tự dòng mới nếu $IFStrống.

Vì vậy, vẫn còn một vấn đề trong đó (printf 'a\nb')(printf 'a\nb\n')mở rộng đến cùng một điều ngay cả với một sản phẩm nào $IFS.

Để giải quyết vấn đề đó, điều tốt nhất tôi có thể nghĩ ra là:

function exact_output
  set -l IFS . # non-empty IFS
  set -l ret
  set -l lines (
    cmd
    set ret $status
    echo
  )
  set -g output ''
  set -l line
  test (count $lines) -le 1; or for line in $lines[1..-2]
    set output $output$line\n
  end
  set output $output$lines[-1]
  return $ret
end

Một cách khác là làm:

read -z output < (begin; cmd; set ret $status; end | psub)

Vỏ Bourne

Vỏ Bourne không hỗ trợ $(...)biểu mẫu cũng như ${var%pattern}toán tử, do đó có thể khá khó để đạt được điều đó. Một cách tiếp cận là sử dụng eval và trích dẫn:

eval "
  output='`
    exec 4>&1
    ret=\`
      exec 3>&1 >&4 4>&-
      (cmd 3>&-; echo \"\$?\" >&3; printf \"'\") |
        awk 3>&- -v RS=\\\\' -v ORS= -v b='\\\\\\\\' '
          NR > 1 {print RS b RS RS}; {print}; END {print RS}'
    \`
    echo \";ret=\$ret\"
  `"

Ở đây, chúng tôi đang tạo ra một

output='output of cmd
with the single quotes escaped as '\''
';ret=X

để được chuyển tới eval. Đối với cách tiếp cận POSIX, nếu 'một trong những ký tự có thể tìm thấy mã hóa ở cuối các ký tự khác, thì chúng ta sẽ gặp vấn đề (một điều tồi tệ hơn nhiều vì nó sẽ trở thành lỗ hổng tiêm lệnh), nhưng may mắn thay, như ., nó không phải là một trong số đó, và kỹ thuật trích dẫn nói chung là một trong những kỹ thuật được sử dụng bởi bất kỳ thứ gì trích dẫn mã shell (lưu ý \có vấn đề, vì vậy không nên sử dụng (cũng loại trừ "..."bên trong bạn cần sử dụng dấu gạch chéo ngược cho một số ký tự) Ở đây, chúng tôi chỉ sử dụng nó sau khi 'ổn.

tcsh

Xem tcsh giữ dòng mới trong thay thế lệnh `...`

(không quan tâm đến trạng thái thoát, mà bạn có thể xử lý bằng cách lưu nó trong một tệp tạm thời ( echo $status > $tempfile:qsau lệnh))


Cảm ơn - và đặc biệt là đầu mối trên các bảng mã khác nhau. Nếu zshcó thể lưu trữ NULtrong một biến, tại sao sẽ không IFS= read -rd '' output < <(cmd)hoạt động? Nó cần có khả năng lưu trữ độ dài của chuỗi ... nó có mã hóa ''dưới dạng chuỗi 1 byte \0thay vì chuỗi 0 byte không?
Tom Hale

1
@TomHale, vâng, read -d ''được coi là read -d $'\0'( bashmặc dù cũng $'\0'giống như ''mọi nơi).
Stéphane Chazelas

Bạn đang kết hợp các ký tự và byte. Vui lòng hiểu rằng nếu chúng tôi xóa chính xác những gì đã được thêm, thực thể ban đầu không được thay đổi. Nó không phải là khó khăn để loại bỏ một byte được gọi xnếu đó là những gì đã được thêm vào. Xin hãy xem câu trả lời đã được chỉnh sửa của tôi.
Mũi tên

@Arrow, vâng, var=value command evalmẹo đã được thảo luận ở đây ( cũng ) và trên danh sách gửi thư của nhóm austin trước đây. Bạn sẽ thấy nó không khả dụng (và khá rõ ràng khi bạn đang thử những thứ như a=1 command eval 'unset a; a=2'hoặc tệ hơn là nó không được sử dụng như vậy). Tương tự như vậy, savedVAR=$VAR;...;VAR=$savedVARđiều đó không làm những gì bạn muốn khi $VARban đầu không được đặt. Nếu đó chỉ là vấn đề lý thuyết (một lỗi không thể khắc phục trong thực tế), thì IMO, nó không đáng để bận tâm. Tuy nhiên, tôi sẽ hỗ trợ bạn vì đã cố gắng.
Stéphane Chazelas

Bạn có một liên kết đến nơi bạn đã loại bỏ và cuối cùng đã loại bỏ việc sử dụng LANG=Cđể loại bỏ một byte khỏi chuỗi không? Bạn đang gây lo ngại xung quanh điểm thực, tất cả đều dễ giải quyết. (1) không có unset được sử dụng (2) Kiểm tra biến trước khi thay đổi nó. @ StéphaneChazelas
Mũi tên

3

Đối với câu hỏi mới, kịch bản này hoạt động:

#!/bin/bash

f()           { for i in $(seq "$((RANDOM % 3 ))"); do
                    echo;
                done; return $((RANDOM % 256));
              }

exact_output(){ out=$( $1; ret=$?; echo x; exit "$ret" );
                unset OldLC_ALL ; [ "${LC_ALL+set}" ] && OldLC_ALL=$LC_ALL
                LC_ALL=C ; out=${out%x};
                unset LC_ALL ; [ "${OldLC_ALL+set}" ] && LC_ALL=$OldLC_ALL
                 printf 'Output:%10q\nExit :%2s\n' "${out}" "$?"
               }

exact_output f
echo Done

Thực hiện:

Output:$'\n\n\n'
Exit :25
Done

Mô tả dài hơn

Sự khôn ngoan thông thường đối với đạn POSIX để đối phó với việc loại bỏ \nlà:

thêm một x

s=$(printf "%s" "${1}x"); s=${s%?}

Điều đó là bắt buộc vì dòng mới ( S ) cuối cùng bị xóa bằng cách mở rộng lệnh trên mỗi đặc tả POSIX :

loại bỏ các chuỗi của một hoặc nhiều ký tự ở cuối thay thế.


Về một dấu vết x.

Người ta đã nói trong câu hỏi này rằng xcó thể bị nhầm lẫn với byte theo sau của một số ký tự trong một số mã hóa. Nhưng làm thế nào chúng ta sẽ đoán những gì hoặc nhân vật nào là tốt hơn trong một số ngôn ngữ trong một số mã hóa có thể, đó là một đề xuất khó khăn, để nói rằng ít nhất.

Tuy nhiên; Điều đó chỉ đơn giản là không chính xác .

Quy tắc duy nhất mà chúng ta cần tuân theo là thêm chính xác những gì chúng ta xóa.

Điều dễ hiểu là nếu chúng ta thêm một cái gì đó vào một chuỗi hiện có (hoặc chuỗi byte) và sau đó chúng ta loại bỏ chính xác cùng một thứ gì đó, thì chuỗi gốc (hoặc chuỗi byte) phải giống nhau.

Chúng ta đi sai ở đâu? Khi chúng ta trộn các ký tựbyte .

Nếu chúng ta thêm một byte, chúng ta phải xóa một byte, nếu chúng ta thêm một ký tự, chúng ta phải xóa chính xác cùng một ký tự .

Tùy chọn thứ hai, thêm một ký tự (và sau đó loại bỏ chính xác cùng một ký tự) có thể trở nên phức tạp và phức tạp, và, vâng, các trang mã và mã hóa có thể gây cản trở.

Tuy nhiên, tùy chọn đầu tiên là hoàn toàn có thể, và sau khi giải thích nó, nó sẽ trở nên đơn giản.

Hãy thêm một byte, một byte ASCII (<127) và để giữ cho mọi thứ càng ít phức tạp càng tốt, giả sử một ký tự ASCII trong phạm vi az. Hoặc như chúng ta nên nói, một byte trong phạm vi hex 0x61- 0x7a. Cho phép chọn bất kỳ trong số đó, có thể là x (thực sự là một byte giá trị 0x78). Chúng ta có thể thêm byte như vậy bằng cách nối một x thành một chuỗi (giả sử một é):

$ a
$ b=${a}x

Nếu chúng ta xem chuỗi là một chuỗi byte, chúng ta sẽ thấy:

$ printf '%s' "$b" | od -vAn -tx1c
  c3  a9  78
 303 251   x

Một chuỗi chuỗi kết thúc bằng một x.

Nếu chúng ta loại bỏ x (giá trị byte 0x78) đó, chúng ta sẽ nhận được:

$ printf '%s' "${b%x}" | od -vAn -tx1c
  c3  a9
 303 251

Nó hoạt động mà không có vấn đề.

Một ví dụ khó khăn hơn một chút.

Hãy nói rằng chuỗi chúng ta quan tâm đến kết thúc bằng byte 0xc3:

$ a=$'\x61\x20\x74\x65\x73\x74\x20\x73\x74\x72\x69\x6e\x67\x20\xc3'

Và cho phép thêm một byte giá trị 0xa9

$ b=$a$'\xa9'

Chuỗi đã trở thành này bây giờ:

$ echo "$b"
a test string é

Chính xác những gì tôi muốn, hai byte cuối cùng là một ký tự trong utf8 (vì vậy bất kỳ ai cũng có thể sao chép kết quả này trong bảng điều khiển utf8 của họ).

Nếu chúng ta xóa một ký tự, chuỗi gốc sẽ được thay đổi. Nhưng đó không phải là những gì chúng tôi đã thêm, chúng tôi đã thêm một giá trị byte, được viết dưới dạng x, nhưng dù sao cũng là một byte.

Những gì chúng ta cần để tránh hiểu sai byte là ký tự. Những gì chúng ta cần là một hành động loại bỏ byte chúng ta đã sử dụng 0xa9. Trong thực tế, tro, bash, lksh và mksh dường như làm chính xác điều đó:

$ c=$'\xa9'
$ echo ${b%$c} | od -vAn -tx1c
 61  20  74  65  73  74  20  73  74  72  69  6e  67  20  c3  0a
  a       t   e   s   t       s   t   r   i   n   g     303  \n

Nhưng không phải ksh hay zsh.

Tuy nhiên, điều đó rất dễ giải quyết, hãy nói với tất cả các shell đó để thực hiện loại bỏ byte:

$ LC_ALL=C; echo ${b%$c} | od -vAn -tx1c 

chỉ vậy thôi, tất cả các shell đã thử nghiệm (trừ yash) (cho phần cuối của chuỗi):

ash             :    s   t   r   i   n   g     303  \n
dash            :    s   t   r   i   n   g     303  \n
zsh/sh          :    s   t   r   i   n   g     303  \n
b203sh          :    s   t   r   i   n   g     303  \n
b204sh          :    s   t   r   i   n   g     303  \n
b205sh          :    s   t   r   i   n   g     303  \n
b30sh           :    s   t   r   i   n   g     303  \n
b32sh           :    s   t   r   i   n   g     303  \n
b41sh           :    s   t   r   i   n   g     303  \n
b42sh           :    s   t   r   i   n   g     303  \n
b43sh           :    s   t   r   i   n   g     303  \n
b44sh           :    s   t   r   i   n   g     303  \n
lksh            :    s   t   r   i   n   g     303  \n
mksh            :    s   t   r   i   n   g     303  \n
ksh93           :    s   t   r   i   n   g     303  \n
attsh           :    s   t   r   i   n   g     303  \n
zsh/ksh         :    s   t   r   i   n   g     303  \n
zsh             :    s   t   r   i   n   g     303  \n

Chỉ đơn giản như vậy, hãy nói với shell để loại bỏ ký tự LC_ALL = C, chính xác là một byte cho tất cả các giá trị byte từ 0x00đến 0xff.

Giải pháp cho ý kiến:

Đối với ví dụ được thảo luận trong các ý kiến, một giải pháp có thể (thất bại trong zsh) là:

#!/bin/bash

LC_ALL=zh_HK.big5hkscs

a=$(printf '\210\170');
b=$(printf '\170');

unset OldLC_ALL ; [ "${LC_ALL+set}" ] && OldLC_ALL=$LC_ALL
LC_ALL=C ; a=${a%"$b"};
unset LC_ALL ; [ "${OldLC_ALL+set}" ] && LC_ALL=$OldLC_ALL

printf '%s' "$a" | od -vAn -c

Điều đó sẽ loại bỏ vấn đề mã hóa.


Điều tốt để biết rằng nhiều hơn một dòng mới có thể được gỡ bỏ.
Tom Hale


Tôi đồng ý rằng việc sửa miền địa phương thành C để đảm bảo ${var%?}luôn luôn cắt một byte là đúng hơn về mặt lý thuyết, nhưng: 1- LC_ALLLC_CTYPEghi đè $LANG, vì vậy bạn cần đặt LC_ALL=C2- bạn không thể thực hiện var=${var%?}trong một mạng con vì sự thay đổi sẽ bị mất, do đó, bạn cần lưu và khôi phục giá trị và trạng thái của LC_ALL(hoặc sử dụng các localtính năng phạm vi không phải POSIX ) 3- thay đổi ngôn ngữ giữa chừng trong tập lệnh không được hỗ trợ đầy đủ trong một số shell như yash. Mặt khác, trong thực tế .không bao giờ là một vấn đề trong các bảng mã ngoài đời thực, vì vậy sử dụng nó để tránh kết hợp với LC_ALL.
Stéphane Chazelas

2

Bạn có thể xuất một ký tự sau đầu ra bình thường và sau đó loại bỏ nó:

#capture the output of "$@" (arguments run as a command)
#into the exact_output` variable
exact_output() 
{
    exact_output=$( "$@" && printf X ) && 
    exact_output=${exact_output%X}
}

Đây là một giải pháp tuân thủ POSIX.


Dựa trên các câu trả lời, tôi thấy câu hỏi của tôi không rõ ràng. Tôi chỉ cập nhật nó.
Tom Hale
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.