Créer ma propre fonction cp dans bash

8

Pour une mission, on me demande d'écrire intelligemment une fonction bash qui a la même fonctionnalité de base que la fonction cp(copie). Il n'a qu'à copier un fichier dans un autre, donc pas de fichiers multiples copiés dans un nouveau répertoire.

Comme je suis nouveau dans la langue bash, je ne comprends pas pourquoi mon programme ne fonctionne pas. La fonction d'origine demande d'écraser un fichier s'il est déjà présent, j'ai donc essayé de l'implémenter. Il échoue.

Il semble que le fichier échoue sur plusieurs lignes, mais surtout à la condition où il vérifie si le fichier à copier existe déjà ( [-e "$2"]). Même ainsi, il affiche toujours le message qui est censé être déclenché si cette condition est remplie (le nom du fichier ...).

Quelqu'un pourrait-il m'aider à corriger ce fichier, fournissant éventuellement des informations utiles dans ma compréhension de base de la langue? Le code est comme suit.

#!/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
timmaay92
la source
17
Commencez avec www.shellcheck.net
Steeldriver
2
Si cela était revue de code: ne jamais faire [ ... ] &> /dev/null. Les tests supportés par [ ... ]sont toujours silencieux, c'est uniquement en cas d'erreurs de syntaxe que toute sortie est produite. Faire taire un tel test, c'est simplement creuser votre propre tombe.
muru
1
Un point: cat $1 | $2"redirige" la sortie de la première commande vers la commande de votre deuxième variable. Mais c'est un nom de fichier, pas le nom d'une commande à exécuter.
Floris
Vous devez revoir la syntaxe de base de la [commande dans votre if.
Barmar
Et vous devez apprendre la différence entre la tuyauterie avec |et la redirection vers un fichier avec >. Ce sont des problèmes de syntaxe de base qui auraient déjà dû être traités dans votre classe.
Barmar

Réponses:

26

L' cputilitaire remplacera volontiers le fichier cible si ce fichier existe déjà, sans inviter l'utilisateur.

Une fonction qui implémente une cpcapacité de base , sans utiliser cpserait

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

Si vous souhaitez inviter l'utilisateur avant d'écraser la cible (notez qu'il peut ne pas être souhaitable de le faire si la fonction est appelée par un shell non interactif):

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

Les messages de diagnostic doivent aller dans le flux d'erreur standard. C'est ce que je fais avec printf ... >&2.

Notez que nous n'avons pas vraiment besoin rmdu fichier cible car la redirection le tronquera. Si nous ne voulons rmd'abord, alors vous devriez vérifier si elle est un répertoire, et si elle est, placez le fichier cible à l' intérieur dans le répertoire correspondant , juste la façon dont le cpferais. Cela fait cela, mais toujours sans explicite 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"
}

Vous pouvez également vous assurer que la source existe réellement, ce qui est le cp cas (le catfait aussi, donc il peut être complètement ignoré, mais cela créerait un fichier cible vide):

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

Cette fonction n'utilise pas de "bashismes" et devrait fonctionner dans des shcoques similaires.

Avec un peu plus de réglages pour prendre en charge plusieurs fichiers source et un -iindicateur qui active l'invite interactive lors du remplacement d'un fichier existant:

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
}

Votre code a de mauvais espacements if [ ... ](besoin d'espace avant et après [et avant ]). Vous ne devez pas non plus essayer de rediriger le test vers /dev/nullcar le test lui-même n'a pas de sortie. Le premier test doit en outre utiliser le paramètre positionnel $2, pas la chaîne file.

En utilisant case ... esaccomme je l'ai fait, vous évitez d'avoir à minuscules / majuscules la réponse de l'utilisateur qui utilise tr. Dans bash, si vous aviez voulu le faire de toute façon, une manière moins coûteuse de le faire aurait été d'utiliser REPLY="${REPLY^^}"(pour les majuscules) ou REPLY="${REPLY,,}"(pour les minuscules).

Si l'utilisateur dit "oui", avec votre code, la fonction met le nom de fichier du fichier cible dans le fichier cible. Ce n'est pas une copie du fichier source. Il doit passer par le bit de copie réel de la fonction.

Le bit de copie est quelque chose que vous avez implémenté à l'aide d'un pipeline. Un pipeline est utilisé pour transmettre des données de la sortie d'une commande à l'entrée d'une autre commande. Ce n'est pas quelque chose que nous devons faire ici. Appelez simplement catle fichier source et redirigez sa sortie vers le fichier cible.

La même chose ne va pas lorsque vous appelez trplus tôt. readdéfinira la valeur d'une variable, mais ne produit aucune sortie, donc le raccordement readà n'importe quoi est absurde.

Aucune sortie explicite n'est nécessaire à moins que l'utilisateur ne dise "non" (ou que la fonction rencontre une condition d'erreur comme dans les bits de mon code, mais puisque c'est une fonction que j'utilise returnplutôt que exit).

Vous avez également dit "fonction", mais votre implémentation est un script.

Jetez un œil à https://www.shellcheck.net/ , c'est un bon outil pour identifier les bits problématiques des scripts shell.


L'utilisation catn'est qu'une façon de copier le contenu d'un fichier. D'autres moyens incluent

  • dd if="$1" of="$2" 2>/dev/null
  • Utiliser n'importe quel utilitaire de type filtre qui peut être fait pour simplement transmettre des données, par exemple sed "" "$1" >"2"ou awk '1' "$1" >"$2"ou tr '.' '.' <"$1" >"$2"etc.
  • etc.

L'astuce consiste à faire en sorte que la fonction copie les métadonnées (propriété et autorisations) de la source vers la cible.

Une autre chose à noter est que la fonction que j'ai écrite se comportera très différemment cpsi la cible est quelque chose comme /dev/ttypar exemple (un fichier non régulier).

Kusalananda
la source
pour ajouter une précision intéressante sur la façon dont cela fonctionne:: cat "$1" >"$2" fonctionnera en 2 fois: le shell voit d'abord > "$2", ce qui signifie: créez-ou-clobber (= vide) le fichier "$ 2", et redirigez la sortie standard de la commande vers ce fichier. Ensuite, il lance la commande cat "$1":, et redirige sa sortie standard vers un fichier "$2"(déjà vidé ou créé vide).
Olivier Dulac
L'une des meilleures réponses que j'ai vues ici! J'ai appris les -efcomparaisons de fichiers et local -a.
gardenhead
@gardenhead Oui, -efprend note si les deux fichiers sont identiques (prend également en charge les liens) et local -acrée une variable de tableau local.
Kusalananda
Merci beaucoup! Vraiment une belle réponse élaborée, vraiment utile pour comprendre la langue aussi
timmaay92