Làm thế nào tôi có thể có được trận đấu đầu tiên từ việc mở rộng ký tự đại diện?


38

Các shell như Bash và Zsh mở rộng ký tự đại diện thành các đối số, càng nhiều đối số khớp với mẫu:

$ echo *.txt
1.txt 2.txt 3.txt

Nhưng nếu tôi chỉ muốn trận đấu đầu tiên được trả lại, không phải tất cả các trận đấu thì sao?

$ echo *.txt
1.txt

Tôi không bận tâm đến các giải pháp dành riêng cho vỏ, nhưng tôi muốn một giải pháp hoạt động với khoảng trắng trong tên tệp.


ls * .txt | đầu -1?
Archemar

1
@Archemar: không hoạt động với dòng mới trong tên tệp.
Flimm

Câu trả lời:


25

Một cách mạnh mẽ trong bash là mở rộng thành một mảng và chỉ xuất phần tử đầu tiên:

pattern="*.txt"
files=( $pattern )
echo "${files[0]}"  # printf is safer!

(Bạn thậm chí có thể echo $files, một chỉ mục bị thiếu được coi là [0].)

Điều này xử lý an toàn không gian / tab / dòng mới và các siêu ký tự khác khi mở rộng tên tệp. Lưu ý rằng cài đặt ngôn ngữ có hiệu lực có thể thay đổi "đầu tiên" là gì.

Bạn cũng có thể thực hiện việc này một cách tương tác với chức năng hoàn thành bash :

_echo() {
    local cur=${COMP_WORDS[COMP_CWORD]}   # string to expand

    if compgen -G "$cur*" > /dev/null; then
        local files=( ${cur:+$cur*} )   # don't expand empty input as *
        [ ${#files} -ge 1 ] && COMPREPLY=( "${files[0]}" )
    fi
}
complete -o bashdefault -F _echo echo

Điều này liên kết _echohàm để hoàn thành các đối số cho echolệnh (ghi đè hoàn thành bình thường). Một "*" bổ sung được thêm vào mã ở trên, bạn chỉ cần nhấn tab vào tên tệp một phần và hy vọng điều đúng sẽ xảy ra.

Mã này hơi phức tạp, thay vì đặt hoặc giả định nullglob( shopt -s nullglob) chúng tôi kiểm tra compgen -Gcó thể mở rộng toàn cầu thành một số kết quả khớp, sau đó chúng tôi mở rộng an toàn thành một mảng và cuối cùng đặt TÍNH TOÁN để trích dẫn được mạnh mẽ.

Bạn có thể thực hiện một phần việc này (lập trình mở rộng toàn cầu) bằng bash compgen -G, nhưng nó không mạnh vì nó xuất ra không được trích dẫn cho thiết bị xuất chuẩn.

Như thường lệ, việc hoàn thành khá khó khăn, điều này phá vỡ sự hoàn thành của những thứ khác, bao gồm các biến môi trường (xem _bash_def_completion()hàm ở đây để biết chi tiết mô phỏng hành vi mặc định).

Bạn cũng có thể chỉ sử dụng compgenbên ngoài chức năng hoàn thành:

files=( $(compgen -W "$pattern") )

Một điểm cần lưu ý là "~" không phải là một quả địa cầu, nó được xử lý bằng bash trong một giai đoạn mở rộng riêng biệt, cũng như các biến $ và các mở rộng khác. compgen -Gchỉ có tên tập tin toàn cầu, nhưng compgen -Wcung cấp cho bạn tất cả các mở rộng mặc định của bash, mặc dù có thể có quá nhiều bản mở rộng (bao gồm ``$()). Không giống như -G, -W được trích dẫn một cách an toàn (tôi không thể giải thích sự chênh lệch). Vì mục đích của -Wnó là mở rộng mã thông báo, điều này có nghĩa là nó sẽ mở rộng "a" thành "a" ngay cả khi không có tệp nào như vậy tồn tại, vì vậy có lẽ nó không lý tưởng.

Điều này dễ hiểu hơn, nhưng có thể có tác dụng phụ không mong muốn:

_echo() {
    local cur=${COMP_WORDS[COMP_CWORD]}
    local files=( $(compgen -W "$cur") ) 
    printf -v COMPREPLY %q "${files[0]}"  
}

Sau đó:

touch $'curious \n filename'

echo curious*tab

Lưu ý việc sử dụng printf %qđể trích dẫn một cách an toàn các giá trị.

Một tùy chọn cuối cùng là sử dụng đầu ra được phân tách bằng 0 với các tiện ích GNU (xem Câu hỏi thường gặp về bash ):

pattern="*.txt"
while IFS= read -r -d $'\0' filename; do 
    printf '%q' "$filename"; 
    break; 
done < <(find . -maxdepth 1 -name "$pattern" -printf "%f\0" | sort -z )

Tùy chọn này cung cấp cho bạn thêm một chút quyền kiểm soát đối với thứ tự sắp xếp (thứ tự khi mở rộng toàn cầu sẽ tùy thuộc vào địa phương của bạn / LC_COLLATEvà có thể hoặc không thể gấp lại trường hợp), nhưng nếu không thì là một cái búa khá lớn cho một vấn đề nhỏ như vậy ;-)


20

Trong zsh, sử dụng [1] vòng loại toàn cầu . Lưu ý rằng mặc dù trường hợp đặc biệt này trả về nhiều nhất một trận đấu, nó vẫn là một danh sách và các khối không được mở rộng trong các ngữ cảnh mong đợi một từ đơn lẻ như bài tập (gán mảng sang một bên).

echo *.txt([1])

Trong ksh hoặc bash, bạn có thể nhồi toàn bộ danh sách các trận đấu trong một mảng và sử dụng phần tử đầu tiên.

tmp=(*.txt)
echo "${tmp[0]}"

Trong bất kỳ shell nào, bạn có thể đặt tham số vị trí và sử dụng tham số đầu tiên.

set -- *.txt
echo "$1"

Điều này làm tắc nghẽn các tham số vị trí. Nếu bạn không muốn điều đó, bạn có thể sử dụng một subshell.

echo "$(set -- *.txt; echo "$1")"

Bạn cũng có thể sử dụng một hàm có tập tham số vị trí riêng.

set_to_first () {
  eval "$1=\"\$2\""
}
set_to_first f *.txt
echo "$f"

1
Và để có được các trận đấu $ n $ đầu tiên, bạn có thể sử dụng*.txt([1,n])
Emre

6

Thử:

for i in *.txt; do printf '%s\n' "$i"; break; done
1.txt

Một lưu ý rằng việc mở rộng tên tệp được sắp xếp theo trình tự đối chiếu có hiệu lực trong miền địa phương hiện tại.



1

Tôi chỉ vấp phải câu hỏi cũ này trong khi tự hỏi điều tương tự. Tôi đã kết thúc với điều này:

echo $(ls *.txt | head -n1)

Tất nhiên, bạn có thể thay thế headbằng tail-n1với bất kỳ số nào khác.


Ở trên sẽ không hoạt động nếu bạn đang làm việc với các tệp có dòng mới trong tên của họ. Để làm việc với các dòng mới, bạn có thể sử dụng bất kỳ trong số này:

  • ls -b *.txt | head -n1 | sed -E 's/\\n/\n/g' (Không hoạt động trên BSD)
  • ls -b *.txt | head -n1 | sed -e 's/\\n/\'$'\n/g'
  • ls -b *.txt | head -n1 | perl -pe 's/\\n/\n/g'
  • echo -e "$(ls -b *.txt | head -n1)" (Hoạt động với bất kỳ ký tự đặc biệt nào)

3
Không, điều đó sẽ thất bại nếu tên tệp có dòng mới.
Isaac

7
Chúng ta đang sống trong thế giới điên rồ nào khi tên tập tin chứa dòng mới?
billynoah

-1

Một trường hợp sử dụng mà tôi thường gặp là xác định thư mục trên / dưới sau khi mở rộng toàn cầu (ví dụ: thư mục chứa đầy đủ SDK phiên bản hoặc công cụ xây dựng). Trong tình huống này, tôi thường muốn lưu tên thư mục đó vào một biến để sử dụng ở một vài vị trí trong tập lệnh shell.

Lệnh này thường làm điều đó cho tôi:

export SDK_DIR=$(dirname /path/to/versioned/sdks/*/. | tail -n1)

Tuyên bố miễn trừ trách nhiệm: Việc mở rộng Glob sẽ không sắp xếp các thư mục của bạn theo semver; Mày đã được cảnh báo. Điều này thật tuyệt nếu bạn có một Dockerfilephiên bản chỉ có một thư mục, nhưng phiên bản thư mục đó có thể thay đổi từ hình ảnh sang hình ảnh


Chào mừng bạn đến với U & L! Điều đó không xử lý hầu hết các tên thư mục, nhưng nó không xử lý các tên thư mục với một dòng mới. Hãy thử tạo một thư mục như thế này mkdir "$(echo one; echo two)"và xem ý tôi là gì.
Flimm

Lợi thế so với các lựa chọn thay thế khác, đặc biệt là phiên bản sử dụng là tailgì?
RalfFriedl

Tiêu chuẩn dirnamechỉ có một tên đường dẫn duy nhất, vì vậy bạn không thể dựa vào nó làm việc trên nhiều tên đường trừ khi bạn biết việc triển khai cụ thể của mình hỗ trợ nó.
Kusalananda

@Flimm Điểm tốt; Tôi nghĩ rằng hầu hết các nhà phát triển sẽ có vấn đề lớn hơn để giải quyết nếu cấu trúc thư mục của họ có dòng mới trong đó ... Tôi chưa bao giờ phải đối phó với điều đó, và đừng mong đợi với bất kỳ phần mềm và phần mềm nửa nào mà tôi đang sử dụng
Andrew Odri

@RalfFriedl Câu hỏi hay; điều này về cơ bản lọc ra bất cứ thứ gì không phải là một thư mục hợp lệ (và sẽ không liệt kê / duyệt qua. và ..)
Andrew Odri
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.