Creare la mia funzione cp in bash


8

Per un compito mi viene chiesto di scrivere abilmente una funzione bash che ha le stesse funzionalità di base della funzione cp(copia). Deve solo copiare un file in un altro, quindi non più file copiati in una nuova directory.

Dato che sono nuovo nella lingua bash, non riesco a capire perché il mio programma non funzioni. La funzione originale chiede di sovrascrivere un file se è già presente, quindi ho provato a implementarlo. Fallisce.

Sembra che il file non riesca su più righe, ma soprattutto alla condizione in cui controlla se il file da copiare è già esistente ( [-e "$2"]). Anche così, mostra ancora il messaggio che dovrebbe essere attivato se tale condizione è soddisfatta (il nome del file ...).

Qualcuno potrebbe aiutarmi a correggere questo file, eventualmente fornendo alcune informazioni utili nella mia comprensione di base della lingua? Il codice è il seguente.

#!/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
Inizia con www.shellcheck.net
steeldriver,

2
Se questa fosse la revisione del codice: non farlo mai[ ... ] &> /dev/null . I test supportati da [ ... ]sono sempre silenziosi, è solo in caso di errori di sintassi che viene prodotto qualsiasi output. Fare tacere un test del genere è semplicemente scavare la propria tomba.
muru,

1
Un punto: cat $1 | $2"convoglia" l'output del primo comando sul comando nella seconda variabile. Ma quello è un nome di file, non il nome di un comando da eseguire.
Floris,

È necessario rivedere la sintassi di base del [comando nel proprio if.
Barmar,

E devi imparare la differenza tra il piping |e il reindirizzamento a un file con >. Questi sono problemi di sintassi di base che avrebbero già dovuto essere trattati nella tua classe.
Barmar,

Risposte:


26

L' cputilità sovrascriverà felicemente il file di destinazione se quel file esiste già, senza richiedere all'utente.

Una funzione che implementa le cpfunzionalità di base , senza usare cpsarebbe

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

Se si desidera richiedere all'utente prima di sovrascrivere la destinazione (si noti che potrebbe non essere desiderabile farlo se la funzione viene chiamata da una shell non interattiva):

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"
}

I messaggi diagnostici dovrebbero passare al flusso di errori standard. Questo è quello che faccio printf ... >&2.

Si noti che non è davvero necessario rmil file di destinazione poiché il reindirizzamento lo troncherà. Se abbiamo fatto vogliamo rmin primo luogo, allora dovreste controllare se si tratta di una directory, e se lo è, metti il file di destinazione all'interno di tale directory invece, proprio come cpfarebbe. Lo sta facendo, ma ancora senza esplicito 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"
}

Potresti anche assicurarti che la fonte esista effettivamente, il che è qualcosa cp che fa (lo catfa anche lui, quindi potrebbe essere lasciato completamente fuori, ovviamente, ma farlo creerebbe un file di destinazione vuoto):

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"
}

Questa funzione non usa "bashismi" e dovrebbe funzionare in tutti gli shshell simili.

Con un po 'più di modifiche per supportare più file di origine e un -iflag che attiva il prompt interattivo quando si sovrascrive un file esistente:

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
}

Il tuo codice ha spaziature errate if [ ... ](serve spazio prima e dopo [e prima ]). Inoltre, non dovresti provare a reindirizzare il test /dev/nullperché il test stesso non ha output. Il primo test dovrebbe inoltre utilizzare il parametro posizionale $2, non la stringa file.

Usando case ... esaccome ho fatto, eviti di dover minuscole / maiuscole la risposta dell'utente che usa tr. In bash, se avessi voluto farlo comunque, un modo più economico di farlo sarebbe stato quello di utilizzare REPLY="${REPLY^^}"(per maiuscole) o REPLY="${REPLY,,}"(per minuscole).

Se l'utente dice "sì", con il tuo codice, la funzione inserisce il nome file del file di destinazione nel file di destinazione. Questa non è una copia del file sorgente. Dovrebbe cadere nel bit di copia effettivo della funzione.

Il bit di copia è qualcosa che hai implementato usando una pipeline. Una pipeline viene utilizzata per passare i dati dall'output di un comando all'input di un altro comando. Questo non è qualcosa che dobbiamo fare qui. Richiama semplicemente catil file di origine e reindirizza il suo output sul file di destinazione.

La stessa cosa è sbagliata quando chiami trprima. readimposterà il valore di una variabile, ma non produce alcun output, quindi il piping readsu qualsiasi cosa è privo di senso.

Non è necessaria alcuna uscita esplicita a meno che l'utente non dica "no" (o la funzione rileva una condizione di errore come nei bit del mio codice, ma poiché è una funzione che uso returnpiuttosto che exit).

Inoltre, hai detto "funzione", ma l'implementazione è uno script.

Dai un'occhiata a https://www.shellcheck.net/ , è un ottimo strumento per identificare bit problematici di script di shell.


L'utilizzo catè solo un modo per copiare il contenuto di un file. Altri modi includono

  • dd if="$1" of="$2" 2>/dev/null
  • Utilizzo di qualsiasi utilità simile a un filtro che può essere creata per passare semplicemente i dati, ad esempio sed "" "$1" >"2"o awk '1' "$1" >"$2"o tr '.' '.' <"$1" >"$2"ecc.
  • eccetera.

Il trucco è che la funzione copia i metadati (proprietà e autorizzazioni) dall'origine alla destinazione.

Un'altra cosa da notare è che la funzione che ho scritto si comporterà in modo abbastanza diverso cpse la destinazione è qualcosa di simile, /dev/ttyad esempio (un file non regolare).


per aggiungere un po 'di precisione interessante su come funziona:: cat "$1" >"$2" funzionerà in 2 volte: la shell vede per prima > "$2"cosa, il che significa: create-or-clobber (= vuoto) il file "$ 2" e reindirizza lo stdout del comando su quel file. Quindi avvia il comando: cat "$1"e reindirizza il suo stdout su file "$2"(già svuotato o creato vuoto).
Olivier Dulac

Una delle migliori risposte che ho visto qui! Ho imparato su entrambi -efi confronti di file e local -a.
gardenhead,

@gardenhead Sì, -efrileva se i due file sono uguali (si occupa anche dei collegamenti) e local -acrea una variabile di matrice locale.
Kusalananda

Grazie mille! Risposta elaborata davvero piacevole, molto utile anche per comprendere la lingua
timmaay92
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.