Thay thế lệnh: tách trên dòng mới nhưng không phải khoảng trắng


30

Tôi biết tôi có thể giải quyết vấn đề này theo nhiều cách, nhưng tôi tự hỏi liệu có cách nào để làm điều đó chỉ bằng cách sử dụng bash dựng sẵn, và nếu không, cách hiệu quả nhất để làm điều đó là gì.

Tôi có một tập tin với nội dung như

AAA
B C DDD
FOO BAR

theo đó tôi chỉ có nghĩa là nó có một vài dòng và mỗi dòng có thể có hoặc không có khoảng trắng. Tôi muốn chạy một lệnh như

cmd AAA "B C DDD" "FOO BAR"

Nếu tôi sử dụng cmd $(< file)tôi nhận được

cmd AAA B C DDD FOO BAR

và nếu tôi sử dụng cmd "$(< file)"tôi nhận được

cmd "AAA B C DDD FOO BAR"

Làm cách nào để mỗi dòng được xử lý chính xác một tham số?


Câu trả lời:


26

Di chuyển

set -f              # turn off globbing
IFS='
'                   # split at newlines only
cmd $(cat <file)
unset IFS
set +f

Hoặc sử dụng một lớp con để làm cho IFStùy chọn và thay đổi cục bộ:

( set -f; IFS='
'; exec cmd $(cat <file) )

Shell thực hiện phân tách trường và tạo tên tệp trên kết quả của một biến hoặc lệnh thay thế không nằm trong dấu ngoặc kép. Vì vậy, bạn cần tắt việc tạo tên tệp với set -fvà định cấu hình chia tách trường IFSđể chỉ tạo các dòng mới tách riêng các trường.

Không có nhiều để đạt được với các cấu trúc bash hoặc ksh. Bạn có thể làm IFScục bộ cho một chức năng, nhưng không set -f.

Trong bash hoặc ksh93, bạn có thể lưu trữ các trường trong một mảng, nếu bạn cần chuyển chúng cho nhiều lệnh. Bạn cần kiểm soát việc mở rộng tại thời điểm bạn xây dựng mảng. Sau đó "${a[@]}"mở rộng đến các phần tử của mảng, mỗi phần một từ.

set -f; IFS=$'\n'
a=($(cat <file))
set +f; unset IFS
cmd "${a[@]}"

10

Bạn có thể làm điều này với một mảng tạm thời.

Thiết lập:

$ cat input
AAA
A B C
DE F
$ cat t.sh
#! /bin/bash
echo "$1"
echo "$2"
echo "$3"

Điền vào mảng:

$ IFS=$'\n'; set -f; foo=($(<input))

Sử dụng mảng:

$ for a in "${foo[@]}" ; do echo "--" "$a" "--" ; done
-- AAA --
-- A B C --
-- DE F --

$ ./t.sh "${foo[@]}"
AAA
A B C
DE F

Không thể tìm ra cách thực hiện mà không có biến tạm thời đó - trừ khi IFSthay đổi không quan trọng đối với cmdtrường hợp:

$ IFS=$'\n'; set -f; cmd $(<input) 

Hãy làm nó.


IFSluôn làm tôi bối rối IFS=$'\n' cmd $(<input)không hoạt động. IFS=$'\n'; cmd $(<input); unset IFSlàm việc. Tại sao? Tôi đoán tôi sẽ sử dụng(IFS=$'\n'; cmd $(<input))
Old Pro

6
@OldPro IFS=$'\n' cmd $(<input)không hoạt động vì nó chỉ đặt IFStrong môi trường của cmd. $(<input)được mở rộng để tạo thành lệnh, trước khi việc gán IFSđược thực hiện.
Gilles 'SO- ngừng trở nên xấu xa'

8

Có vẻ như cách thức kinh điển để làm điều này bashlà một cái gì đó giống như

unset args
while IFS= read -r line; do 
    args+=("$line") 
done < file

cmd "${args[@]}"

hoặc, nếu phiên bản bash của bạn có mapfile:

mapfile -t args < filename
cmd "${args[@]}"

Sự khác biệt duy nhất tôi có thể tìm thấy giữa mapfile và vòng lặp while-đọc so với một-liner

(set -f; IFS=$'\n'; cmd $(<file))

là cái trước sẽ chuyển đổi một dòng trống thành một đối số trống, trong khi một dòng sẽ bỏ qua một dòng trống. Trong trường hợp này, hành vi một lớp là những gì tôi thích hơn, vì vậy, gấp đôi số tiền thưởng cho nó là nhỏ gọn.

Tôi sẽ sử dụng IFS=$'\n' cmd $(<file)nhưng nó không hoạt động, bởi vì $(<file)được giải thích để tạo thành dòng lệnh trước khi IFS=$'\n'có hiệu lực.

Mặc dù nó không hoạt động trong trường hợp của tôi, nhưng bây giờ tôi đã học được rằng rất nhiều công cụ hỗ trợ chấm dứt các dòng null (\000)thay vì newline (\n)điều đó giúp cho việc này dễ dàng hơn khi xử lý, ví dụ như tên tệp, là nguồn phổ biến của các tình huống này :

find / -name '*.config' -print0 | xargs -0 md5

cung cấp danh sách các tên tệp đủ điều kiện dưới dạng đối số cho md5 mà không có bất kỳ nội dung hoặc nội suy hoặc bất cứ điều gì. Điều đó dẫn đến giải pháp không tích hợp

tr "\n" "\000" <file | xargs -0 cmd

mặc dù điều này cũng vậy, bỏ qua các dòng trống, mặc dù nó có các dòng chỉ có khoảng trắng.


Sử dụng cmd $(<file)các giá trị mà không trích dẫn (sử dụng khả năng bash để phân chia các từ) luôn là một đặt cược rủi ro. Nếu bất kỳ dòng nào, *nó sẽ được shell mở rộng thành một danh sách các tệp.

3

Bạn có thể sử dụng bash tích hợp mapfileđể đọc tệp thành một mảng

mapfile -t foo < filename
cmd "${foo[@]}"

hoặc, chưa được kiểm tra, xargscó thể làm điều đó

xargs cmd < filename

Từ tài liệu mapfile: "mapfile không phải là tính năng shell phổ biến hoặc di động". Và thực sự là nó không được hỗ trợ trên hệ thống của tôi. xargscũng không giúp được gì
Old Pro

Bạn sẽ cần xargs -dhoặcxargs -L
James Youngman

@James, không, tôi không có -dtùy chọn và xargs -L 1chạy lệnh một lần trên mỗi dòng nhưng vẫn phân tách các đối số trên khoảng trắng.
Old Pro

1
@OldPro, bạn cũng đã yêu cầu "một cách để làm điều đó chỉ bằng cách sử dụng bash dựng sẵn" thay vì "một tính năng vỏ phổ biến hoặc di động". Nếu phiên bản bash của bạn quá cũ, bạn có thể cập nhật nó không?
glenn jackman

mapfilerất tiện dụng đối với tôi, vì nó lấy các dòng trống làm các mục mảng, điều mà IFSphương thức không làm được. IFScoi các dòng mới liền kề như một dấu phân cách duy nhất ... Cảm ơn bạn đã trình bày nó, vì tôi không biết về lệnh này (mặc dù, dựa trên dữ liệu đầu vào của OP và dòng lệnh dự kiến, có vẻ như anh ấy thực sự muốn bỏ qua các dòng trống).
Peter.O

0
old=$IFS
IFS='  #newline
'
array=`cat Submissions` #input the text in this variable
for ...  #use parts of variable in the for loop
... 
done
IFS=$old

Cách tốt nhất tôi có thể tìm thấy. Chỉ cần hoạt động.


Và tại sao nó hoạt động nếu bạn đặt IFSthành không gian, nhưng câu hỏi là không phân chia trên không gian?
RalfFriedl

0

Tập tin

Vòng lặp cơ bản nhất (di động) để phân chia tệp trên dòng mới là:

#!/bin/sh
while read -r line; do            # get one line (\n) at a time.
    set -- "$@" "$line"           # store in the list of positional arguments.
done <infile                      # read from a file called infile.
printf '<%s>' "$@" ; echo         # print the results.

Mà sẽ in:

$ ./script
<AAA><A B C><DE F>

Có, với IFS mặc định = spacetabnewline.

Tại sao nó hoạt động

  • IFS sẽ được shell sử dụng để phân chia đầu vào thành nhiều biến. Vì chỉ có một biến, nên không có sự phân tách nào được thực hiện bởi trình bao. Vì vậy, không có thay đổi IFScần thiết.
  • Có, không gian / tab hàng đầu và dấu đang bị xóa, nhưng dường như không có vấn đề gì trong trường hợp này.
  • Không, không có Globing được thực hiện vì không mở rộng là không được trích dẫn . Vì vậy, không set -fcần thiết.
  • Mảng duy nhất được sử dụng (hoặc cần thiết) là các tham số vị trí giống như mảng.
  • Các -r(nguyên) lựa chọn là để tránh việc loại bỏ hầu hết các dấu chéo ngược.

Điều đó sẽ không hoạt động nếu cần tách và / hoặc Globing. Trong những trường hợp như vậy, một cấu trúc phức tạp hơn là cần thiết.

Nếu bạn cần (vẫn di động) để:

  • Tránh loại bỏ các khoảng trống / tab hàng đầu và dấu, sử dụng: IFS= read -r line
  • Tách dòng cho vars trên một số ký tự, sử dụng : IFS=':' read -r a b c.

Tách tệp trên một số ký tự khác (không di động, hoạt động với ksh, bash, zsh):

IFS=':' read -d '+' -r a b c

Sự bành trướng

Tất nhiên, tiêu đề của câu hỏi của bạn là về việc chia tách một lệnh thực thi trên dòng mới để tránh sự phân chia trên khoảng trắng.

Cách duy nhất để có được tách từ trình bao là để lại một bản mở rộng mà không có dấu ngoặc kép:

echo $(< file)

Điều đó được kiểm soát bởi giá trị của IFS, và, trên các bản mở rộng không được trích dẫn, Globing cũng được áp dụng. Để mkae làm việc, bạn cần:

  • Chỉ đặt IFS thành dòng mới , để chỉ tách trên dòng mới.
  • Bỏ đặt tùy chọn vỏ hình cầu set +f:

    đặt + f IFS = '' cmd $ (<tệp)

Tất nhiên, điều đó thay đổi giá trị của IFS và toàn cầu cho phần còn lại của tập lệnh.

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.