Cách tách đầu ra lệnh thành từng dòng riêng lẻ


12
list=`ls -a R*`
echo $list

Trong tập lệnh shell, lệnh echo này sẽ liệt kê tất cả các tệp từ thư mục hiện tại bắt đầu bằng R, nhưng trong một dòng. Làm thế nào tôi có thể in từng mục trên một dòng?

Tôi cần một lệnh chung cho tất cả các tình huống xảy ra với ls, du, find -type -dvv


6
Lưu ý chung: không làm điều này với ls, nó sẽ phá vỡ mọi tên tập tin kỳ lạ . Xem ở đây .
terdon

Câu trả lời:


12

Nếu đầu ra của lệnh chứa nhiều dòng, sau đó trích dẫn các biến của bạn để duy trì các dòng mới đó khi lặp lại:

echo "$list"

Nếu không, shell sẽ mở rộng và phân chia nội dung biến trên khoảng trắng (khoảng trắng, dòng mới, tab) và những nội dung đó sẽ bị mất.


15

Thay vì vô tình đưa lsđầu ra vào một biến và sau đó echoing nó, loại bỏ tất cả các màu, sử dụng

ls -a1

Từ man ls

       -1     list one file per line.  Avoid '\n' with -q or -b

Tôi không khuyên bạn làm bất cứ điều gì với đầu ra của ls, ngoại trừ hiển thị nó :)

Sử dụng, ví dụ, các khối vỏ và một forvòng lặp để làm một cái gì đó với các tệp ...

shopt -s dotglob                  # to include hidden files*

for i in R/*; do echo "$i"; done

* Này sẽ không bao gồm thư mục hiện hành .hoặc cha mẹ của nó ..mặc dù


1
Tôi khuyên bạn nên thay thế: echo "$i"bằng printf "%s\n" "$i" (cái đó sẽ không bị nghẹt nếu tên tệp bắt đầu bằng "-"). Trên thực tế, hầu hết thời gian echo somethingnên được thay thế bằng printf "%s\n" "something".
Olivier Dulac

2
@OlivierDulac Tất cả đều bắt đầu từ Rđây, nhưng nói chung, vâng. Tuy nhiên, ngay cả đối với kịch bản, việc cố gắng luôn luôn dẫn đầu -trong các đường dẫn gây ra rắc rối: mọi người giả định --, chỉ một số lệnh hỗ trợ, biểu thị kết thúc của các tùy chọn. Tôi đã thấy find . [tests] -exec [cmd] -- {} \;nơi [cmd]không hỗ trợ --. Mọi con đường đều bắt đầu bằng .mọi cách! Nhưng đó không phải là lý lẽ chống lại việc thay thế echobằng printf '%s\n'. Ở đây, người ta có thể thay thế toàn bộ vòng lặp với printf '%s\n' R/*. Bash có printfdạng dựng sẵn, do đó, không có giới hạn về số lượng / độ dài của các đối số.
Eliah Kagan

12

Mặc dù đặt nó trong dấu ngoặc kép như @muru đề xuất sẽ thực sự làm những gì bạn yêu cầu, bạn cũng có thể muốn xem xét sử dụng một mảng cho việc này. Ví dụ:

IFS=$'\n' dirs=( $(find . -type d) )

Các IFS=$'\n'bảo bash để chỉ chia sản lượng trên xuống dòng characcters o lấy mỗi phần tử của mảng. Không có nó, nó sẽ phân chia trên các khoảng trắng, do đó a file name with spaces.txtsẽ là 5 phần tử riêng biệt thay vì một phần tử. Cách tiếp cận này sẽ bị hỏng nếu tên tệp / thư mục của bạn có thể chứa dòng mới ( \n). Nó sẽ lưu từng dòng đầu ra của lệnh dưới dạng một phần tử của mảng.

Lưu ý rằng tôi cũng đã thay đổi kiểu cũ `command`để $(command)đó là cú pháp ưa thích.

Bây giờ bạn có một mảng được gọi $dirs, mỗi bof có các phần tử là một dòng đầu ra của lệnh trước đó. Ví dụ:

$ find . -type d
.
./olad
./ho
./ha
./ads
./bar
./ga
./da
$ IFS=$'\n' dirs=( $(find . -type d) )
$ for d in "${dirs[@]}"; do
    echo "DIR: $d"
  done
DIR: .
DIR: ./olad
DIR: ./ho
DIR: ./ha
DIR: ./ads
DIR: ./bar
DIR: ./ga
DIR: ./da

Bây giờ, vì một số lạ bash (xem tại đây ), bạn sẽ cần phải đặt IFSlại về giá trị ban đầu sau khi thực hiện việc này. Vì vậy, hoặc lưu nó và sắp xếp lại:

oldIFS="$IFS"
IFS=$'\n' dirs=( $(find . -type d) ) 
IFS="$oldIFS"

Hoặc làm thủ công:

IFS=" "$'\t\n '

Ngoài ra, chỉ cần đóng thiết bị đầu cuối hiện tại. Cái mới của bạn sẽ có IFS gốc được thiết lập lại.


Tôi có thể đã đề xuất một cái gì đó như thế này, nhưng sau đó phần về dulàm cho nó trông OP quan tâm hơn đến việc giữ định dạng đầu ra.
muru

Làm thế nào tôi có thể làm cho công việc này nếu tên thư mục chứa khoảng trắng?
tráng miệng

@dPS whoops! Nó nên hoạt động với không gian (nhưng không phải là dòng mới) bây giờ. Cảm ơn đã chỉ ra điều đó.
terdon

1
Lưu ý rằng bạn nên đặt lại hoặc hủy đặt IFS sau khi gán mảng đó. unix.stackexchange.com/q/264635/70524
muru

@muru oh wow, cảm ơn, tôi đã quên mất sự kỳ lạ đó.
terdon

2

Nếu bạn phải lưu trữ đầu ra và bạn muốn một mảng, mapfilelàm cho nó dễ dàng.

Trước tiên, hãy xem xét nếu bạn cần lưu trữ đầu ra của lệnh của bạn. Nếu bạn không, chỉ cần chạy lệnh.

Nếu bạn quyết định muốn đọc đầu ra của một lệnh dưới dạng một mảng các dòng, thì đúng là có một cách để làm là vô hiệu hóa tính năng tạo khối, thiết lập IFSđể phân tách trên các dòng, sử dụng thay thế lệnh trong ( )cú pháp tạo mảng và đặt lại IFSsau đó, đó là phức tạp hơn nó dường như. Câu trả lời của terdon bao gồm một số cách tiếp cận đó. Nhưng tôi khuyên bạn nên sử dụng mapfile, lệnh tích hợp của shell Bash để đọc văn bản dưới dạng một dòng các dòng, thay vào đó. Đây là một trường hợp đơn giản khi bạn đọc từ một tệp:

mapfile < filename

Điều này đọc các dòng thành một mảng được gọi là MAPFILE. Nếu không có chuyển hướng đầu vào , bạn sẽ đọc từ đầu vào tiêu chuẩn của shell (thường là thiết bị đầu cuối của bạn) thay vì tệp có tên . Để đọc vào một mảng khác với mặc định , hãy truyền tên của nó. Ví dụ, điều này đọc vào mảng :< filenamefilenameMAPFILElines

mapfile lines < filename

Một hành vi mặc định khác mà bạn có thể chọn để thay đổi là các ký tự dòng mới ở cuối dòng được đặt đúng chỗ; chúng xuất hiện dưới dạng ký tự cuối cùng trong mỗi thành phần mảng (trừ khi đầu vào kết thúc mà không có ký tự dòng mới, trong trường hợp đó, phần tử cuối cùng không có ký tự). Để kiểm tra các dòng mới này để chúng không xuất hiện trong mảng, hãy chuyển -ttùy chọn sang mapfile. Ví dụ, điều này đọc vào mảng recordsvà không ghi các ký tự dòng mới vào các thành phần mảng của nó:

mapfile -t records < filename

Bạn cũng có thể sử dụng -tmà không cần chuyển tên mảng; đó là, nó hoạt động với một tên mảng ẩn của MAPFILEquá.

Các mapfilevỏ được xây dựng trong hỗ trợ tùy chọn khác, và nó cách khác có thể được gọi là readarray. Chạy help mapfile(và help readarray) để biết chi tiết.

Nhưng bạn không muốn đọc từ tệp, bạn muốn đọc từ đầu ra của lệnh. Để đạt được điều đó, sử dụng quá trình thay thế . Lệnh này đọc các dòng từ lệnh some-commandvới arguments...các đối số dòng lệnh của nó và đặt chúng trong mapfilemảng mặc định MAPFILEcủa chúng, với các ký tự dòng mới kết thúc của chúng bị xóa:

mapfile -t < <(some-command arguments...)

Quá trình thay thế thay thế bằng một tên tệp thực tế mà từ đó đầu ra của việc chạy có thể được đọc. Tệp này là một ống có tên chứ không phải là tệp thông thường và trên Ubuntu, nó sẽ được đặt tên như (đôi khi với một số khác ), nhưng bạn không cần phải quan tâm đến các chi tiết, vì trình bao quan tâm đến nó Tất cả đằng sau hậu trường.<(some-command arguments...)some-command arguments.../dev/fd/6363

Bạn có thể nghĩ rằng bạn có thể từ bỏ quá trình thay thế bằng cách sử dụng thay vào đó, nhưng điều đó sẽ không hiệu quả bởi vì, khi bạn có một đường dẫn gồm nhiều lệnh được phân tách bằng cách , Bash chạy tất cả các lệnh trong các chuỗi con . Do đó, cả hai và chạy trong môi trường riêng của chúng , được khởi tạo từ nhưng tách biệt với môi trường của vỏ mà bạn chạy đường ống. Trong subshell nơi chạy, mảng sẽ được điền, nhưng sau đó mảng đó bị loại bỏ khi lệnh kết thúc. không bao giờ được tạo hoặc sửa đổi cho người gọi.some-command arguments... | mapfile -t|some-command arguments...mapfile -tmapfile -tMAPFILEMAPFILE

Dưới đây là toàn bộ ví dụ trong câu trả lời của terdon , nếu bạn sử dụng mapfile:

mapfile -t dirs < <(find . -type d)
for d in "${dirs[@]}"; do
    echo "DIR: $d"
done

Đó là nó. Bạn không cần kiểm tra xem IFSđã được đặt chưa , theo dõi xem nó có được đặt hay không và với giá trị nào, đặt thành dòng mới, sau đó đặt lại hoặc hủy đặt lại sau. Bạn không cần phải vô hiệu hóa globbing (ví dụ, với set -f) - mà thực sự là cần thiết nếu bạn đang sử dụng phương pháp mà nghiêm túc, vì tên tập tin có thể chứa *, ?[--then tái kích hoạt nó ( set +f) sau đó.

Bạn cũng có thể thay thế hoàn toàn vòng lặp cụ thể đó bằng một printflệnh duy nhất - mặc dù điều này thực sự không phải là lợi ích của nó mapfile, vì bạn có thể làm điều đó cho dù bạn sử dụng mapfilehay phương thức khác để điền vào mảng. Đây là phiên bản ngắn hơn:

mapfile -t < <(find . -type d)
printf 'DIR: %s\n' "${MAPFILE[@]}"

Điều quan trọng cần ghi nhớ là, như terdon đề cập , hoạt động từng dòng không phải lúc nào cũng phù hợp và sẽ không hoạt động chính xác với tên tệp có chứa dòng mới. Tôi khuyên bạn không nên đặt tên tệp theo cách đó, nhưng nó có thể xảy ra, bao gồm cả tình cờ.

Thực sự không có một giải pháp nào phù hợp cho tất cả mọi người.

Bạn đã yêu cầu "một lệnh chung cho tất cả các kịch bản" và cách tiếp cận sử dụng mapfilephần nào cách tiếp cận mục tiêu đó, nhưng tôi khuyên bạn nên xem xét lại các yêu cầu của mình.

Nhiệm vụ hiển thị ở trên đạt được tốt hơn chỉ với một findlệnh:

find . -type d -printf 'DIR: %p\n'

Bạn cũng có thể sử dụng một lệnh bên ngoài như sedđể thêm DIR: vào đầu mỗi dòng. Điều này được cho là hơi xấu, và không giống như lệnh find, nó sẽ thêm "tiền tố" bổ sung vào tên tệp có chứa dòng mới, nhưng nó hoạt động bất kể đầu vào của nó để đáp ứng yêu cầu của bạn và vẫn nên đọc đầu ra vào biến hoặc mảng :

find . -type d | sed 's/^/DIR: /'

Nếu bạn cần liệt kê và cũng thực hiện một hành động trên mỗi thư mục được tìm thấy, chẳng hạn như chạy some-commandvà chuyển đường dẫn của thư mục làm đối số, findcho phép bạn cũng làm điều đó:

find . -type d -print -exec some-command {} \;

Một ví dụ khác, chúng ta hãy quay trở lại nhiệm vụ chung là thêm tiền tố vào mỗi dòng. Giả sử tôi muốn xem đầu ra của help mapfilenhưng đánh số các dòng. Tôi thực sự sẽ không sử dụng mapfilecho điều này, cũng như bất kỳ phương pháp nào khác đọc nó vào một biến shell hoặc mảng shell. Giả sử help mapfile | cat -nkhông đưa ra định dạng tôi muốn, tôi có thể sử dụng awk:

help mapfile | awk '{ printf "%3d: %s\n", NR, $0 }'

Đọc tất cả đầu ra của một lệnh vào một biến hoặc mảng đôi khi hữu ích và phù hợp, nhưng nó có nhược điểm lớn. Bạn không chỉ phải đối phó với sự phức tạp bổ sung của việc sử dụng trình bao của mình khi một lệnh hiện có hoặc kết hợp các lệnh hiện có có thể đã làm những gì bạn cần hoặc tốt hơn, nhưng toàn bộ đầu ra của lệnh phải được lưu trữ trong bộ nhớ. Đôi khi bạn có thể biết đó không phải là vấn đề, nhưng đôi khi bạn có thể đang xử lý một tệp lớn.

Một cách khác thường được cố gắng - và đôi khi được thực hiện đúng - là đọc từng dòng đầu vào với read -rmột vòng lặp. Nếu bạn không cần lưu trữ các dòng trước trong khi hoạt động trên các dòng sau và bạn phải sử dụng đầu vào dài, thì nó có thể tốt hơn mapfile. Nhưng nó cũng nên tránh trong các trường hợp khi bạn có thể chuyển nó thành một lệnh có thể thực hiện công việc, đó là hầu hết các trường hợp .

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.