Criando minha própria função cp no bash

8

Para uma tarefa me pedem para escrever inteligentemente uma função bash que tenha a mesma funcionalidade básica da função cp (copy). Só tem que copiar um arquivo para outro, então nenhum arquivo é copiado para um novo diretório.

Como sou novo na linguagem bash, não consigo entender por que meu programa não está funcionando. A função original pede para sobrescrever um arquivo, se ele já estiver presente, então tentei implementar isso. Isso falha.

O arquivo falha em várias linhas, parece, mas mais importante na condição em que ele verifica se o arquivo a ser copiado já existe ( [-e "$2"] ). Mesmo assim, ele ainda mostra a mensagem que deve ser acionada se essa condição for atendida (o nome do arquivo ...).

Alguém poderia me ajudar a consertar este arquivo, possivelmente fornecendo algumas informações úteis na minha compreensão básica do idioma? O código é o seguinte.

#!/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
    
por timmaay92 22.05.2017 / 17:50

1 resposta

26

O utilitário cp ficará feliz em sobrescrever o arquivo de destino se esse arquivo já existir, sem avisar o usuário.

Uma função que implementa a capacidade básica de cp , sem usar cp , seria

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

Se você quiser avisar o usuário antes de sobrescrever o alvo (note que pode não ser desejável fazer isso se a função for chamada por um shell não interativo):

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

As mensagens de diagnóstico devem ir para o fluxo de erro padrão. Isso é o que eu faço com printf ... >&2 .

Observe que realmente não precisamos rm do arquivo de destino, pois o redirecionamento truncará o arquivo. Se nós fizemos queremos rm primeiro, então você teria que verificar se é um diretório, e se for, coloque o arquivo de destino dentro daquele diretório, da mesma forma que cp faria. Isso está fazendo isso, mas ainda sem rm explícito:

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

Você também pode querer ter certeza de que a fonte realmente existe, o que é algo que cp faz ( cat também, então pode ser deixado de fora completamente, é claro, mas isso criaria um arquivo de destino vazio):

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

Esta função não usa "bashisms" e deve funcionar em todas as shells parecidas com sh .

Com um pouco mais de ajustes para suportar vários arquivos de origem e um sinalizador -i que ativa o aviso interativo ao sobrescrever um arquivo existente:

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
}

Seu código tem maus espaçamentos em if [ ... ] (precisa de espaço antes e depois de [ e antes de ] ). Você também não deve tentar redirecionar o teste para /dev/null , pois o teste em si não tem saída. O primeiro teste deve, além disso, usar o parâmetro posicional $2 , não a string file .

Usando case ... esac como eu fiz, você evita que a resposta do usuário seja minúscula / maiúscula usando tr . Em bash , se você quisesse fazer isso de qualquer maneira, uma maneira mais barata de fazer isso seria usar REPLY="${REPLY^^}" (para maiúsculas) ou REPLY="${REPLY,,}" (para minúsculas).

Se o usuário disser "sim", com seu código, a função colocará o nome do arquivo de destino no arquivo de destino. Esta não é uma cópia do arquivo de origem. Ele deve cair até o bit de cópia real da função.

O bit de cópia é algo que você implementou usando um pipeline. Um pipeline é usado para transmitir dados da saída de um comando para a entrada de outro comando. Isso não é algo que precisamos fazer aqui. Basta invocar cat no arquivo de origem e redirecionar sua saída para o arquivo de destino.

O mesmo acontece com você chamando tr antes. read definirá o valor de uma variável, mas não produzirá nenhuma saída, portanto, canalizar read para qualquer coisa é sem sentido.

Nenhuma saída explícita é necessária a menos que o usuário diga "não" (ou a função encontre alguma condição de erro como em bits do meu código, mas como é uma função eu uso return em vez de exit ).

Além disso, você disse "função", mas sua implementação é um script.

Dê uma olhada no link , é uma boa ferramenta para identificar bits problemáticos de scripts de shell.

Usar o modo cat é apenas um para copiar o conteúdo de um arquivo. Outras maneiras incluem

  • dd if="$1" of="$2" 2>/dev/null
  • Uso de qualquer utilitário do tipo filtro que pode ser usado apenas para transmitir dados, por exemplo sed "" "$1" >"2" ou awk '1' "$1" >"$2" ou tr '.' '.' <"$1" >"$2" etc.
  • etc.

O problema é fazer com que a função copie os metadados (propriedade e permissões) da origem para o destino.

Outra coisa a notar é que a função que escrevi se comportará de maneira bem diferente de cp se o destino for algo como /dev/tty , por exemplo (um arquivo não regular).

    
por 22.05.2017 / 18:17