Nivelar diretório, mas preservar nomes de diretório no novo nome de arquivo

10

Como posso nivelar um diretório no seguinte formato?

Antes: ./aaa/bbb/ccc.png

Depois de: ./aaa-bbb-ccc.png

Nathan JB
fonte
PS: Por favor, também são responsáveis por nomes de arquivos com caracteres estranhos (por exemplo, espaços)
Nathan JB
3
Você também vai precisar para especificar como você deseja lidar com o caso possível, onde ./foo/bar/baz.pnge ./foo-bar-baz.pngambos existem. Presumo que você não queira substituir o último pelo primeiro?
Jw013
No meu caso, é irrelevante, mas a resposta deve de fato explicar isso para que outros possam confiar nela! Obrigado!
Nathan JB

Respostas:

7

Aviso: digitei a maioria desses comandos diretamente no meu navegador. Advertência.

Com zsh e zmv :

zmv -o -i -Qn '(**/)(*)(D)' '${1//\//-}$2'

Explicação: O padrão **/*corresponde a todos os arquivos nos subdiretórios do diretório atual, recursivamente (não corresponde aos arquivos no diretório atual, mas estes não precisam ser renomeados). Os dois primeiros pares de parênteses são grupos que podem ser referidos como $1e $2no texto de substituição. O par final de parênteses adiciona o D qualificador glob para que os arquivos de ponto não sejam omitidos. -o -isignifica passar a -iopção para, mvpara que você seja solicitado se um arquivo existente será substituído.


Com apenas ferramentas POSIX:

find . -depth -exec sh -c '
    for source; do
      case $source in ./*/*)
        target="$(printf %sz "${source#./}" | tr / -)";
        mv -i -- "$source" "${target%z}";;
      esac
    done
' _ {} +

Explicação: a caseinstrução omite o diretório atual e os subdiretórios de nível superior do diretório atual. targetcontém o nome do arquivo de origem ( $0) com o primeiro destacado ./e todas as barras substituídas por traços, além de uma final z. A final zestá lá, caso o nome do arquivo termine com uma nova linha: caso contrário, a substituição do comando o removeria.

Se o seu findnão suporta -exec … +(OpenBSD, estou olhando para você):

find . -depth -exec sh -c '
    case $0 in ./*/*)
      target="$(printf %sz "${0#./}" | tr / -)";
      mv -i -- "$0" "${target%z}";;
    esac
' {} \;

Com o bash (ou ksh93), você não precisa chamar um comando externo para substituir as barras por traços, você pode usar a expansão de parâmetro ksh93 com a construção de substituição de cadeia ${VAR//STRING/REPLACEMENT}:

find . -depth -exec bash -c '
    for source; do
      case $source in ./*/*)
        source=${source#./}
        target="${source//\//-}";
        mv -i -- "$source" "$target";;
      esac
    done
' _ {} +
Gilles 'SO- parar de ser mau'
fonte
Os for sourceexemplos perdem o primeiro arquivo / diretório apresentado ... Finalmente descobri por que você (normalmente) usou _como $0:) ... e obrigado pelas ótimas respostas. Eu aprendo muito com eles.
Peter.O
Observe que o exemplo zmv apenas executa uma execução a seco: imprime os comandos de movimentação, mas não os executa. Para realmente executar esses comandos, remova o n em -Qn.
Peter
5

Embora já existam boas respostas aqui, acho isso mais intuitivo em bash:

find aaa -type f -exec sh -c 'new=$(echo "{}" | tr "/" "-" | tr " " "_"); mv "{}" "$new"' \;

Onde aaaestá o diretório existente. Os arquivos que ele contém serão movidos para o diretório atual (deixando apenas diretórios vazios no aaa). As duas chamadas para trlidar com diretórios e espaços (sem lógica para barras escapadas - um exercício para o leitor).

Considero isso mais intuitivo e relativamente fácil de ajustar. Ou seja, eu poderia alterar os findparâmetros se disser que quero encontrar apenas arquivos .png. Posso alterar as trchamadas ou adicionar mais conforme necessário. E posso acrescentar echoantes mvse quiser verificar visualmente se ele fará a coisa certa (depois repita sem o extra echoapenas se as coisas parecerem corretas:

find aaa -name \*.png -exec sh -c 'new=$(echo "{}" | tr "/" "-" | tr " " "_"); echo mv "{}" "$new"' \;

Observe também que estou usando find aaa... e não find .... ou find aaa/..., pois os dois últimos deixariam artefatos engraçados no nome final do arquivo.

Dave Cohen
fonte
Obrigado por este liner - isso funcionou bem para mim quando o fiz fora do diretório (que é exatamente o que você disse para fazer). Quando tentei fazer de dentro do diretório (esperando evitar que o nome do diretório raiz fosse adicionado) todos os arquivos "desapareceram". Eu os havia transformado em arquivos ocultos no mesmo diretório.
Dgig
2
find . -mindepth 2 -type f -name '*' |
  perl -l000ne 'print $_;  s/\//-/g; s/^\.-/.\// and print' |
    xargs -0n2 mv 

Nota: isso não funcionará para o nome do arquivo que contém \n.
Isso, é claro, move apenas farquivos de tipo ...
Os únicos conflitos de nome seriam de arquivos pré-existentes no pwd

Testado com este subconjunto básico

rm -fr junk
rm -f  junk*hello*

mkdir -p  junk/junkier/junkiest
touch    'hello    hello'
touch    'junk/hello    hello'
touch    'junk/junkier/hello    hello'
touch    'junk/junkier/junkiest/hello    hello'

Resultando em

./hello    hello
./junk-hello    hello
./junk-junkier-hello    hello
./junk-junkier-junkiest-hello    hello
Peter.O
fonte
0

Aqui está uma lsversão .. comentários são bem-vindos. Ele funciona para nomes de arquivos malucos como esse "j1ळ\n\001\n/j2/j3/j4/\nh  h\n", mas estou realmente interessado em saber de alguma armadilha. É mais de um exercício de "pode o caluniado lsrealmente fazê-lo (robusta)?"

ls -AbQR1p * |                        # paths in "quoted" \escaped format
    sed -n '/":$/,${/.[^/]$/p}' |     # skip files in pwd, blank and dir/ lines
    { bwd="$PWD"; while read -n1;     # save base dir strip leading "quote
                        read -r p; do # read path
        printf -vs "${p%\"*}"         # strip trailing quote"(:)
        [[ $p == *: ]] && {           # this is a directory
            cd   "$bwd/$s"            # change into new directory
            rnp="${s//\//-}"-         # relative name prefix
        } || mv "$s" "$bwd/$rnp$s"
      done; }
Peter.O
fonte
-1

Basta canalizar o nome do arquivo + caminho para o trcomando.tr <replace> <with>

for FILE in `find aaa -name "*.png"`
do
  NEWFILE=`echo $FILE | tr '/' '-'`
  cp -v $FILE $NEWFILE
done
emcconville
fonte
1
Isso não lida com nomes de arquivos estranhos com segurança. Os espaços vão quebrá-lo (entre outras coisas).
Jw013
À primeira vista, esta é uma solução limpa e legível, mas @ jw013 está certo, ele não manipula bem os espaços. Mudar a cplinha para cp -v "$FILE" "$NEWFILE"ajudar? (Além disso, você não deve assumir apenas os arquivos PNG.)
Nathan JB
2
@ NathanJ.Brauer A adição de aspas à cplinha é insuficiente. Toda a abordagem de analisar a findsaída com um forloop é falha. As únicas maneiras seguras de usar findsão com -exec, ou -print0se disponíveis (menos portáteis que -exec). Sugiro uma alternativa mais robusta, mas sua pergunta está subespecificada no momento.
Jw013
-2

Seguro para espaços em nomes de arquivos:

#!/bin/bash

/bin/find $PWD -type f | while read FILE
do
    _new="${FILE//\//-}"
    _new="${_new:1}"
    #echo $FILE
    #echo $_new

    /bin/mv "$FILE" "$_new"
done

Funcionou com o meu em cpvez de mvpor razões óbvias, mas deve produzir o mesmo.

Tim
fonte
3
type find-> find is /usr/bin/findna minha máquina. Usar PATHcomo nome de variável em um script é uma péssima idéia. Além disso, seu script manipula os nomes de arquivos com espaços ou barras invertidas, porque você usa o readcomando incorretamente (pesquise neste site IFS read -r).
Gilles 'SO- stop be evil'
find is hashed (/bin/find), relevante como? Distros diferentes são diferentes?
Tim
Sabia que eu deveria ter ficado longe disso, isca de abutre.
Tim
@ Tim Você sempre pode excluir a resposta para obter seu representante de volta (e o meu também porque custa rep para votar). A codificação dos caminhos dos scripts parece sugerir que você está perdendo o ponto da PATHvariável de ambiente, que é algo que todo sistema Unix usa. Se você escrever /bin/find, seu script será realmente quebrado em todos os sistemas (Gilles, meu e provavelmente o OP) que não possui /bin/find(por exemplo, b / c está /usr/bin/find). O ponto principal da PATHvariável de ambiente é tornar isso um problema.
Jw013
By the way, $(pwd)já está disponível em uma variável: $PWD. Mas você não deve usar um caminho absoluto aqui: se seu script for invocado, digamos /home/tim, ele tenta renomear /home/tim/some/pathpara /home-tim-some-path.
Gilles 'SO- stop be evil'