Làm cách nào để lưu / dev / stdout vị trí mục tiêu trong tập lệnh bash?


12

Tôi có một tập lệnh bash nhất định, muốn giữ /dev/stdoutvị trí ban đầu trước khi thay thế bộ mô tả tệp 1 bằng vị trí khác.

Vì vậy, một cách tự nhiên, tôi đã viết một cái gì đó như

old_stdout=$(readlink -f /dev/stdout)

Và nó đã không hoạt động. Rất nhanh tôi hiểu vấn đề là gì:

test@ubuntu:~$ echo $(readlink -f /dev/stdout)
/proc/5175/fd/pipe:[31764]
test@ubuntu:~$ readlink -f /dev/stdout
/dev/pts/18

Obvioulsly, $()chạy trong một lớp con, được dẫn đến vỏ mẹ.

Vì vậy, câu hỏi là: có cách nào đáng tin cậy (nằm trong phạm vi tính di động giữa các bản phân phối Linux) để lưu /dev/stdoutvị trí dưới dạng chuỗi trong tập lệnh bash không?


Điều này nghe có vẻ giống như một vấn đề XY . Vấn đề cơ bản là gì?
Kusalananda

Vấn đề cơ bản là một tập lệnh cài đặt nhất định chạy ở hai chế độ - im lặng, trong đó nó ghi nhật ký tất cả đầu ra vào tệp và dài dòng, trong đó nó không chỉ ghi nhật ký vào tệp mà còn in mọi thứ sang thiết bị đầu cuối. Nhưng trong cả hai chế độ, tập lệnh muốn tương tác với người dùng, tức là in ra thiết bị đầu cuối và đọc phản hồi của người dùng. Vì vậy, tôi nghĩ rằng tiết kiệm /dev/stdoutsẽ giải quyết vấn đề với việc in tin nhắn ở chế độ im lặng. Thay thế là chuyển hướng mọi hành động khác tạo ra đầu ra và có khá nhiều trong số chúng. Gần gấp 100 lần so với tin nhắn tương tác của người dùng.
alexey.e.egorov

Cách tương tác tiêu chuẩn với người dùng là in ra stderr. Đây là, ví dụ, tại sao các lời nhắc sẽ stderrđược mặc định.
Kusalananda

Thật không may, stderrcũng phải được chuyển hướng và lưu lại, vì tập lệnh gọi một số chương trình bên ngoài và tất cả các thông báo lỗi có thể sẽ được thu thập và ghi lại.
alexey.e.egorov

Câu trả lời:


14

Để lưu một mô tả tập tin, bạn nhân đôi nó trên một fd khác. Lưu một đường dẫn đến tệp tương ứng là không đủ, bạn cần lưu chế độ mở, cờ mở, vị trí hiện tại trong tệp, v.v. Và tất nhiên, đối với các đường ống ẩn danh hoặc ổ cắm, điều đó sẽ không hoạt động vì những đường ống này không có đường dẫn. Những gì bạn muốn lưu là mô tả tệp mở mà fd đề cập đến và sao chép một fd thực sự trả về một fd mới cho cùng một mô tả tệp mở .

Để sao chép một bộ mô tả tệp lên một cái khác, với shell giống như Bourne, cú pháp là:

exec 3>&1

Ở trên, fd 1 được nhân đôi lên fd 3.

Bất cứ điều gì fd 3 đã được mở trước đó sẽ bị đóng, nhưng lưu ý rằng fds 3 đến 9 (thường là nhiều hơn, lên tới 99 với yash) được dành riêng cho mục đích đó (và không có ý nghĩa đặc biệt trái với 0, 1 hoặc 2), Shell biết không sử dụng chúng cho kinh doanh nội bộ của riêng mình. Lý do duy nhất fd 3 sẽ được mở trước đó là vì bạn đã thực hiện nó trong kịch bản 1 hoặc nó đã bị rò rỉ bởi người gọi.

Sau đó, bạn có thể thay đổi thiết bị xuất chuẩn sang thứ khác:

exec > /dev/null

Và sau đó, để khôi phục thiết bị xuất chuẩn:

exec >&3 3>&-

( 3>&-để đóng bộ mô tả tập tin mà chúng ta không còn cần nữa).

Bây giờ, vấn đề với điều đó là ngoại trừ trong ksh, mọi lệnh bạn chạy sau đó exec 3>&1sẽ thừa hưởng fd 3. Đó là một rò rỉ fd. Nói chung không phải là một vấn đề lớn, nhưng điều đó có thể gây ra vấn đề.

kshđặt cờ close-on-exec trên các fds đó (đối với fds trên 2), nhưng không phải các shell khác và các shell khác không có cách nào để đặt cờ đó theo cách thủ công.

Công việc xung quanh cho shell khác là đóng fd 3 cho mỗi và mọi lệnh, như:

exec 3>&-

exec > file.log

ls 3>&-
uname 3>&-

exec >&3 3>&-

Cồng kềnh. Ở đây, cách tốt nhất là không sử dụng exec, mà chuyển hướng các nhóm lệnh:

{
  ls
  uname
} > file.log

Ở đó, lớp vỏ cần chú ý để lưu thiết bị xuất chuẩn và khôi phục nó sau đó (và nó thực hiện nó bên trong bằng cách sao chép nó trên một fd (trên 9, trên 99 đối với yash) với bộ cờ close-on-exec ).

Lưu ý 1

Bây giờ, việc quản lý các fds 3 đến 9 đó có thể trở nên cồng kềnh và có vấn đề nếu bạn sử dụng chúng rộng rãi hoặc trong các chức năng, đặc biệt nếu tập lệnh của bạn sử dụng một số mã bên thứ ba có thể lần lượt sử dụng các fds đó.

Một số vỏ ( zsh, bash, ksh93, tất cả các bổ sung tính năng ( được đề xuất bởi Oliver người hay nói đùa củazsh ) trong khoảng thời gian tương tự vào năm 2005 sau khi nó đã được thảo luận giữa các nhà phát triển của họ) có một cú pháp thay thế cho giao fd miễn phí đầu tiên trên 10 thay vì giúp trong trường hợp này:

myfunction() {
  local fd
  exec {fd}>&1
  # stdout was duplicated onto a new fd above 10, whose actual value
  # is stored in the fd variable
  ...
  # it should even be safe to re-enter the function here
  ...
  exec >&"$fd" {fd}>&-
}

Ngoài ra, mã của bạn sai theo nghĩa là fd 3 có thể đã được sử dụng, vì nó xảy ra khi một đoạn mã chạy từ một rc.localdịch vụ, ví dụ: Vì vậy, bạn thực sự đã sử dụng một cái gì đó như exec {FD}>&1hoặc một cái gì đó. Nhưng điều này chỉ được hỗ trợ trong bash 4, điều này thực sự đáng buồn. Vì vậy, đây không thực sự di động.
alexey.e.egorov

@ alexey.e.egorov, xem chỉnh sửa.
Stéphane Chazelas

Bash 3. * không hỗ trợ tính năng này và phiên bản này được sử dụng trong Centos 5, vẫn được hỗ trợ và vẫn được sử dụng. Và tìm mô tả miễn phí và sau đó eval "exec $i>&1"là một điều tôi muốn tránh, do nó rườm rà. Tôi thực sự có thể dựa vào fds trên 9 sẽ miễn phí không?
alexey.e.egorov

@ alexey.e.egorov, không, bạn đang nhìn nó lạc hậu. fds 3 đến 9 được sử dụng miễn phí (và tùy thuộc vào bạn để quản lý chúng theo ý muốn) và được dành cho mục đích đó. fds trên 9 có thể được sử dụng bởi shell trong nội bộ và đóng chúng có thể gây ra hậu quả khó chịu. Hầu hết các vỏ sẽ không cho phép bạn sử dụng chúng. bashsẽ để bạn tự bắn vào chân mình.
Stéphane Chazelas

2
@ alexey.e.egorov, nếu khi bắt đầu, tập lệnh của bạn có một số fds trong (3..9) mở, đó là vì người gọi của bạn đã quên đóng chúng hoặc đặt cờ close-on-exec trên chúng. Đó là những gì tôi gọi là rò rỉ fd. Bây giờ, có thể người gọi dự định chuyển các fds đó cho bạn, vì vậy bạn có thể đọc và / hoặc ghi dữ liệu từ / cho họ, nhưng sau đó bạn sẽ biết về nó. Nếu bạn không biết về họ, thì bạn không quan tâm, sau đó bạn có thể đóng chúng một cách tự do (lưu ý rằng nó chỉ đóng quy trình fd của tập lệnh của bạn chứ không phải của người gọi của bạn).
Stéphane Chazelas

3

Như bạn có thể thấy, bash scripting không giống như một ngôn ngữ lập trình thông thường nơi bạn có thể gán các mô tả tệp.

Giải pháp đơn giản nhất là sử dụng lớp vỏ phụ để chạy những gì bạn muốn chuyển hướng để quá trình xử lý có thể được hoàn nguyên về lớp vỏ trên cùng có nguyên vẹn I / O tiêu chuẩn.

Một giải pháp thay thế sẽ là sử dụng ttyđể xác định thiết bị TTY và điều khiển I / O trong tập lệnh của bạn. Ví dụ:

dev=$(tty)

và sau đó bạn có thể ..

echo message > $dev

> Một giải pháp thay thế sẽ là sử dụng tty để xác định thiết bị TTY và điều khiển I / O trong tập lệnh của bạn. Làm thế nào một người làm điều này?
alexey.e.egorov

1
Tôi chỉ bao gồm một ví dụ trong câu trả lời của tôi.
Julie Pelletier

1

$$ sẽ cung cấp cho bạn quy trình PID hiện tại, trong trường hợp shell tương tác hoặc tập lệnh PID shell liên quan.

Vì vậy, bạn có thể sử dụng:

readlink -f /proc/$$/fd/1

Thí dụ:

% readlink -f /proc/$$/fd/1
/dev/pts/33

% var=$(readlink -f /proc/$$/fd/1)

% echo $var                       
/dev/pts/33

1
Mặc dù nó hoạt động, việc dựa vào một /proccấu trúc cụ thể gây ra các vấn đề về tính di động, cũng như việc sử dụng /dev/stdoutnhư được đề cập trong câu hỏi.
Julie Pelletier

1
@JuliePelletier Dựa vào /proccấu trúc cụ thể? Nó sẽ hoạt động trên bất kỳ Linux nào có procfs..
heemayl

1
Đúng vậy, vì vậy chúng ta có thể khái quát hóa cho Linux như procfshầu như luôn luôn có mặt, nhưng chúng ta thường thấy các câu hỏi về tính di động và một phương pháp phát triển tốt bao gồm xem xét tính di động đối với các hệ thống khác. bashcó thể chạy trên vô số hệ điều hành.
Julie Pelletier
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.