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

8

Para uma tarefa, sou solicitado a escrever de forma inteligente uma função bash que possui a mesma funcionalidade básica que a função cp(cópia). Ele só precisa copiar um arquivo para outro, portanto, nenhum arquivo múltiplo é copiado para um novo diretório.

Como eu sou novo no idioma bash, não consigo entender por que meu programa não está funcionando. A função original pede para substituir um arquivo, se ele já estiver presente, então tentei implementá-lo. Falha.

Parece que o arquivo falha em várias linhas, mas o mais importante é na condição em que 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 corrigir esse 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
timmaay92
fonte
17
Comece com www.shellcheck.net
steeldriver
2
Se isso fosse uma revisão de código: nunca faça isso [ ... ] &> /dev/null. Os testes suportados [ ... ]são sempre silenciosos; é apenas no caso de erros de sintaxe que qualquer saída é produzida. Silenciar esse teste é simplesmente cavar sua própria cova.
muru
1
Um ponto: cat $1 | $2"canaliza" a saída do primeiro comando para o comando na sua segunda variável. Mas esse é um nome de arquivo, não o nome de um comando a ser executado.
Floris 23/05
Você precisa revisar a sintaxe básica do [comando no seu if.
Barmar
E você precisa aprender a diferença entre canalizar |e redirecionar para um arquivo com >. Esses são problemas básicos de sintaxe que já deveriam ter sido abordados em sua classe.
Barmar

Respostas:

26

O cputilitário substituirá alegremente o arquivo de destino se esse arquivo já existir, sem avisar o usuário.

Uma função que implementa a cpcapacidade básica , sem o uso, cpseria

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

Se você deseja avisar o usuário antes de substituir o destino (observe 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 erros padrão. É com isso que eu faço printf ... >&2.

Observe que realmente não precisamos rmdo arquivo de destino, pois o redirecionamento o truncará. Se se quiser rmlo primeiro, então você teria que verificar se é um diretório, e se for, coloque o arquivo de destino dentro deste diretório em vez disso, apenas a forma como cpfaria. Isso é feito, mas ainda sem explícito 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"
}

Você também pode ter certeza de que a fonte realmente existe, o que é algo cp que faz ( cattambém existe, portanto pode ser deixada 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 "bashismos" e deve funcionar em shconchas de todos os tipos.

Com um pouco mais de ajustes para oferecer suporte a vários arquivos de origem e um -isinalizador que ativa o prompt interativo ao substituir 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 espaçamentos incorretos if [ ... ](precisa de espaço antes e depois [e antes ]). Você também não deve tentar redirecionar o teste, /dev/nullpois o teste em si não possui saída. Além disso, o primeiro teste deve usar o parâmetro posicional $2, não a string file.

Usando case ... esaccomo eu fiz, você evita ter que minúsculas / maiúsculas a resposta do usuário usando tr. Em bash, se você queria fazer isso de qualquer maneira, uma maneira mais barata de fazê-lo teria sido a utilização REPLY="${REPLY^^}"(para maiúscula) ou REPLY="${REPLY,,}"(para lowercasing).

Se o usuário disser "sim", com o seu código, a função colocará o nome do arquivo do arquivo de destino no arquivo de destino. Esta não é uma cópia do arquivo de origem. Ele deve passar para 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 passar dados da saída de um comando para a entrada de outro comando. Isso não é algo que precisamos fazer aqui. Simplesmente invoque cato arquivo de origem e redirecione sua saída para o arquivo de destino.

A mesma coisa está errada com você ligando trantes. readdefinirá o valor de uma variável, mas não produzirá saída; portanto, a canalização readpara qualquer coisa é absurda.

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

Você também disse "função", mas sua implementação é um script.

Dê uma olhada em https://www.shellcheck.net/ , é uma boa ferramenta para identificar bits problemáticos de scripts de shell.


O uso caté apenas uma maneira de copiar o conteúdo de um arquivo. Outras maneiras incluem

  • dd if="$1" of="$2" 2>/dev/null
  • Utilizando qualquer utilitário semelhante a um filtro, que pode ser feito apenas para passar dados, por exemplo, sed "" "$1" >"2"ou awk '1' "$1" >"$2"ou tr '.' '.' <"$1" >"$2"etc.
  • etc.

A parte complicada é 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 eu escrevi se comportará de maneira bem diferente de cpse o destino for algo como, /dev/ttypor exemplo (um arquivo não regular).

Kusalananda
fonte
para adicionar alguma precisão interessante sobre como ele funciona:: cat "$1" >"$2" funcionará em 2 vezes: o shell primeiro vê > "$2", o que significa: create-or-clobber (= empty) o arquivo "$ 2" e redireciona o stdout do comando para esse arquivo. Em seguida, ele lança o comando cat "$1":, e redireciona seu stdout para o arquivo "$2"(já esvaziado ou criado vazio).
Olivier Dulac
Uma das melhores respostas que eu já vi aqui! Eu aprendi sobre as -efcomparações de arquivos e local -a.
Gardenhead 23/05
@ardenhead Sim, -efobserva se os dois arquivos são iguais (também cuida dos links) e local -acria uma variável de matriz local.
Kusalananda
Muito obrigado! Realmente agradável resposta elaborada, realmente útil em compreender a linguagem bem
timmaay92