Tạo hàm cp của riêng tôi trong bash


8

Đối với một bài tập, tôi được yêu cầu viết khéo léo một hàm bash có chức năng cơ bản giống như hàm cp(bản sao). Nó chỉ phải sao chép một tập tin này sang tập tin khác, vì vậy không có nhiều tập tin được sao chép vào một thư mục mới.

Vì tôi chưa quen với ngôn ngữ bash, tôi không thể hiểu tại sao chương trình của tôi không hoạt động. Hàm ban đầu yêu cầu ghi đè lên một tệp nếu nó đã có sẵn, vì vậy tôi đã cố gắng thực hiện điều đó. Nó thất bại.

Có vẻ như tệp bị lỗi ở nhiều dòng, nhưng quan trọng nhất là ở điều kiện nó kiểm tra xem tệp có được sao chép để tồn tại không ( [-e "$2"]). Mặc dù vậy, nó vẫn hiển thị thông báo được cho là được kích hoạt nếu điều kiện đó được đáp ứng (Tên tệp ...).

Bất cứ ai có thể giúp tôi sửa chữa tập tin này, có thể cung cấp một số cái nhìn sâu sắc hữu ích trong sự hiểu biết cơ bản của tôi về ngôn ngữ? Mã như sau.

#!/bin/sh
echo "file variable: $2"
if [-e file]&> /dev/null
then
    echo "The file name already exists, want to overwrite? (yes/no)"
    read  | tr "[A-Z]" "[a-z]"
    if [$REPLY -eq "yes"] ; then
        rm "$2"
        echo $2 > "$2"
        exit 0
    else
        exit 1
    fi
else
    cat $1 | $2
    exit 0
fi

17
Bắt đầu với www.shellcheck.net
Steeldo

2
Nếu đây là đánh giá mã: đừng bao giờ làm [ ... ] &> /dev/null. Các bài kiểm tra được hỗ trợ [ ... ]luôn im lặng, chỉ trong trường hợp có lỗi cú pháp mà bất kỳ đầu ra nào cũng được tạo ra. Im lặng một bài kiểm tra như vậy chỉ đơn giản là đào mộ của chính bạn.
muru

1
Một điểm: cat $1 | $2"ống dẫn" đầu ra của lệnh đầu tiên đến lệnh trong biến thứ hai của bạn. Nhưng đó là tên tệp chứ không phải tên lệnh để thực thi.
Floris

Bạn cần xem lại cú pháp cơ bản của [lệnh trong if.
Barmar

Và bạn cần tìm hiểu sự khác biệt giữa đường ống với |và chuyển hướng đến một tệp với >. Đây là những vấn đề cú pháp cơ bản cần có trong lớp của bạn.
Barmar

Câu trả lời:


26

Các cptiện ích hạnh phúc sẽ ghi đè lên tập tin mục tiêu nếu tập tin đã tồn tại, mà không nhắc người sử dụng.

Một chức năng thực hiện cpkhả năng cơ bản , mà không sử dụng cpsẽ là

cp () {
    cat "$1" >"$2"
}

Nếu bạn muốn nhắc người dùng trước khi ghi đè mục tiêu (lưu ý rằng có thể không mong muốn thực hiện việc này nếu chức năng được gọi bởi trình bao không tương tác):

cp () {
    if [ -e "$2" ]; then
        printf '"%s" exists, overwrite (y/n): ' "$2" >&2
        read
        case "$REPLY" in
            n*|N*) return ;;
        esac
    fi

    cat "$1" >"$2"
}

Các thông báo chẩn đoán sẽ đi đến luồng lỗi tiêu chuẩn. Đây là những gì tôi làm với printf ... >&2.

Lưu ý rằng chúng ta không thực sự cần đến rmtệp đích vì chuyển hướng sẽ cắt bớt nó. Nếu chúng tôi thực sự muốn rmnó trước tiên, thì bạn sẽ phải kiểm tra xem đó có phải là một thư mục hay không và nếu có, hãy đặt tệp mục tiêu vào trong thư mục đó thay vào đó, chỉ là cách cpsẽ làm. Điều này đang làm điều đó, nhưng vẫn không rõ ràng rm:

cp () {
    target="$2"
    if [ -d "$target" ]; then
        target="$target/$1"
    fi

    if [ -d "$target" ]; then
        printf '"%s": is a directory\n' "$target" >&2
        return 1
    fi

    if [ -e "$target" ]; then
        printf '"%s" exists, overwrite (y/n): ' "$target" >&2
        read
        case "$REPLY" in
            n*|N*) return ;;
        esac
    fi

    cat "$1" >"$target"
}

Bạn cũng có thể muốn đảm bảo rằng nguồn thực sự tồn tại, đó là điều cp cần làm ( catcũng vậy, vì vậy nó có thể bị bỏ hoàn toàn, tất nhiên, nhưng làm như vậy sẽ tạo ra một tệp mục tiêu trống):

cp () {
    if [ ! -f "$1" ]; then
        printf '"%s": no such file\n' "$1" >&2
        return 1
    fi

    target="$2"
    if [ -d "$target" ]; then
        target="$target/$1"
    fi

    if [ -d "$target" ]; then
        printf '"%s": is a directory\n' "$target" >&2
        return 1
    fi

    if [ -e "$target" ]; then
        printf '"%s" exists, overwrite (y/n): ' "$target" >&2
        read
        case "$REPLY" in
            n*|N*) return ;;
        esac
    fi

    cat "$1" >"$target"
}

Hàm này không sử dụng "bashism" và sẽ hoạt động trong các shshell giống như.

Với một chút tinh chỉnh để hỗ trợ nhiều tệp nguồn và -icờ kích hoạt lời nhắc tương tác khi ghi đè lên tệp hiện có:

cp () {
    local interactive=0

    # Handle the optional -i flag
    case "$1" in
        -i) interactive=1
            shift ;;
    esac

    # All command line arguments (not -i)
    local -a argv=( "$@" )

    # The target is at the end of argv, pull it off from there
    local target="${argv[-1]}"
    unset argv[-1]

    # Get the source file names
    local -a sources=( "${argv[@]}" )

    for source in "${sources[@]}"; do
        # Skip source files that do not exist
        if [ ! -f "$source" ]; then
            printf '"%s": no such file\n' "$source" >&2
            continue
        fi

        local _target="$target"

        if [ -d "$_target" ]; then
            # Target is a directory, put file inside
            _target="$_target/$source"
        elif (( ${#sources[@]} > 1 )); then
            # More than one source, target needs to be a directory
            printf '"%s": not a directory\n' "$target" >&2
            return 1
        fi

        if [ -d "$_target" ]; then
            # Target can not be overwritten, is directory
            printf '"%s": is a directory\n' "$_target" >&2
            continue
        fi

        if [ "$source" -ef "$_target" ]; then
            printf '"%s" and "%s" are the same file\n' "$source" "$_target" >&2
            continue
        fi

        if [ -e "$_target" ] && (( interactive )); then
            # Prompt user for overwriting target file
            printf '"%s" exists, overwrite (y/n): ' "$_target" >&2
            read
            case "$REPLY" in
                n*|N*) continue ;;
            esac
        fi

        cat -- "$source" >"$_target"
    done
}

Mã của bạn có khoảng cách xấu trong if [ ... ](cần không gian trước và sau [và trước ]). Bạn cũng không nên thử chuyển hướng kiểm tra đến /dev/nullvì bản thân bài kiểm tra không có đầu ra. Thử nghiệm đầu tiên nên sử dụng tham số vị trí $2, không phải chuỗi file.

Sử dụng case ... esacnhư tôi đã làm, bạn tránh phải viết thường / viết hoa phản hồi từ người dùng bằng cách sử dụng tr. Trong bash, nếu bạn đã muốn làm điều này bằng mọi cách, một cách rẻ hơn để làm điều đó sẽ là sử dụng REPLY="${REPLY^^}"(cho cấp trên) hoặc REPLY="${REPLY,,}"(cho hạ cấp).

Nếu người dùng nói "có", với mã của bạn, hàm sẽ đặt tên tệp của tệp đích vào tệp đích. Đây không phải là một bản sao của tập tin nguồn. Nó sẽ rơi vào bit sao chép thực tế của hàm.

Bit sao chép là thứ bạn đã thực hiện bằng cách sử dụng đường ống dẫn. Một đường ống được sử dụng để truyền dữ liệu từ đầu ra của một lệnh sang đầu vào của một lệnh khác. Đây không phải là điều chúng ta cần làm ở đây. Chỉ cần gọi cattệp nguồn và chuyển hướng đầu ra của nó sang tệp đích.

Điều tương tự là sai với bạn gọi trtrước đó. readsẽ đặt giá trị của một biến, nhưng không tạo ra đầu ra, do đó, đường ống readtới bất cứ thứ gì là vô nghĩa.

Không có lối thoát rõ ràng là cần thiết trừ khi người dùng nói "không" (hoặc hàm gặp phải một số điều kiện lỗi như trong các bit của mã của tôi, nhưng vì đó là hàm tôi sử dụng returnchứ không phải exit).

Ngoài ra, bạn đã nói "chức năng", nhưng việc thực hiện của bạn là một tập lệnh.

Hãy xem https://www.shellcheck.net/ , đây là một công cụ tốt để xác định các bit có vấn đề của các tập lệnh shell.


Sử dụng catchỉ là một cách để sao chép nội dung của tệp. Những cách khác bao gồm

  • dd if="$1" of="$2" 2>/dev/null
  • Sử dụng bất kỳ tiện ích giống như bộ lọc nào có thể được tạo để chỉ truyền dữ liệu qua, ví dụ sed "" "$1" >"2"hoặc awk '1' "$1" >"$2"hoặc tr '.' '.' <"$1" >"$2"v.v.
  • Vân vân.

Một mẹo nhỏ là làm cho hàm sao chép siêu dữ liệu (quyền sở hữu và quyền) từ nguồn sang đích.

Một điều cần chú ý là chức năng tôi đã viết sẽ hoạt động hoàn toàn khác so với cpnếu mục tiêu là một cái gì đó giống như /dev/ttyví dụ (một tệp không thông thường).


để thêm một số độ chính xác can thiệp vào cách thức hoạt động :: cat "$1" >"$2" sẽ hoạt động trong 2 lần: shell lần đầu tiên nhìn thấy > "$2", có nghĩa là: tạo-hoặc-clobber (= trống) tệp "$ 2" và chuyển hướng stdout của lệnh sang tệp đó. Sau đó, nó khởi chạy lệnh : cat "$1", và chuyển hướng thiết bị xuất chuẩn của nó thành tệp "$2"(đã được làm trống hoặc tạo trống).
Olivier Dulac

Một trong những câu trả lời hay nhất tôi từng thấy ở đây! Tôi đã học về cả hai -efso sánh tập tin và local -a.
vườn

@gardenhead Có, -efsẽ thông báo nếu hai tệp giống nhau (cũng quan tâm đến các liên kết) và local -atạo một biến mảng cục bộ.
Kusalananda

Cảm ơn bạn rất nhiều! Câu trả lời công phu thực sự tốt đẹp, cũng rất hữu ích trong việc hiểu ngôn ngữ
timmaay92
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.