Сценарии оболочки: выберите папку на основе части имени файла

Мой проект

Я создаю сценарий оболочки bash для выполнения из терминала. Его цель — архивировать множество папок проекта. Каждая папка следует предписанной номенклатуре: [YYYY.MM.DD] - Medium - Client - Project name - details--details - JobNumber. Например: [2006.02.01] - Print - Development - Appeal I - Kids Art Show Insert - D0601-11. Эти проекты в настоящее время являются одной папкой. Я хочу отсортировать их по папкам по имени клиента. Есть 7 (внутренних) клиентов, поэтому я использую следующий сценарий оболочки:

#!/bin/bash

# Go to the Completed Projects folder.
cd /Volumes/communications/Projects/Completed\ Projects/

# Find a folder with a specified string (e.g. "Academics") in its name.
# Move (not copy) the folder to its corresponding sub-folder of the Archived Projects folder. (e.g. /Academics)

for folder in *; do
    if [[ -d "$folder" ]]; then
        if [[ "$folder" == *Academics* ]]; then
            echo "Archiving $folder to Archived Projects → Academics...";
            mv "$folder" /Volumes/communications/Projects/Archived\ Projects/Academics/
        fi
        elif [[ "$folder" == *Admissions* ]]; then
            echo "Archiving $folder to Archived Projects → Admissions...";
            mv "$folder" /Volumes/communications/Projects/Archived\ Projects/Admissions/
        fi
        elif [[ "$folder" == *Alumni* ]]; then
            echo "Archiving $folder to Archived Projects → Academics...";
            mv "$folder" /Volumes/communications/Projects/Archived\ Projects/Alumni/
        fi
        elif [[ "$folder" == *Communications* ]]; then
            echo "Archiving $folder to Archived Projects → Academics...";
            mv "$folder" /Volumes/communications/Projects/Archived\ Projects/Communications/
        fi
        elif [[ "$folder" == *Development* ]]; then
            echo "Archiving $folder to Archived Projects → Academics...";
            mv "$folder" /Volumes/communications/Projects/Archived\ Projects/Development/
        fi
        elif [[ "$folder" == *President* ]]; then
            echo "Archiving $folder to Archived Projects → Academics...";
            mv "$folder" /Volumes/communications/Projects/Archived\ Projects/President/
        fi
        elif [[ "$folder" == *Student\ Life* ]]; then
            echo "Archiving $folder to Archived Projects → Academics...";
            mv "$folder" /Volumes/communications/Projects/Archived\ Projects/Student\ Life/
        fi
    else #Folders that don't match the pattern prompt the use to move them by hand.
        echo "$folder does not have a Department name. Move it by 
done

Моя проблема

Мой сценарий будет неправильно анализировать и неправильно записывать проект с именем [2006.03.01] - Print - Development - Academics and Accreditation - D0601-08. Он будет читать "Академики" еще до того, как попадет в условное для клиента "Развитие". В результате это будут файлы в «Академики». И мне пришлось бы выковыривать его вручную!

Преимущество моей системы

Мои коллеги и я тщательно подходили к нашей номенклатуре (описанной выше). Я знаю, что имя клиента находится между 2-м и 3-м дефисом.

Мой вопрос

Как использовать преимущество моей системы для решения моей проблемы? Я хочу, чтобы этот сценарий соответствовал только той части имени папки, которая идет после первых двух дефисов и перед третьим дефисом, т. е. я хочу, чтобы этот сценарий выполнял поиск только в «поле» клиента в имени папки. Я продолжаю думать о «регулярных выражениях», но понятия не имею, как их реализовать.

Примечание. Я предпочитаю, чтобы решение дополняло мой текущий скрипт, а не заменяло его. Я пришел к этому через @patrix на этом сайте, и его идея позволила обойти некоторые ошибки.

Почему баш ? Если бы я мог дать вам сценарий на другом языке, вы бы согласились?
Хороший вопрос, @IanC. Bash, потому что это все, что я знаю, как использовать терминал в Mac OS X.
bash - это ограниченный язык, поскольку ОС Unix теперь поставляются с такими языками, как perl python и т. д. Я бы написал что-нибудь длиннее 3-4 строк, так как bash ведет себя не очень хорошо.
*- Academics -*?
я обновил свой ответ
Я знаю, о чем вы спрашиваете, и это комментарий, а не ответ. Думали ли вы об использовании меток в сочетании с find? Это может быть именно то, что вы хотите. Проверьте @grgarside apple.stackexchange.com/questions/131164/…
На данный момент ни один из ответов, которые мы дали, не относится конкретно к Mac. Не могли бы вы использовать что-то вроде Hazel для управления папкой?
Я бы подумал о Хейзел, но я пытаюсь научиться писать сценарии. Также Hazel кажется идеальным для локального хранилища, но я работаю с сетевым ресурсом под управлением Windows 2012 Server. @IanC.

Ответы (3)

Есть несколько способов сделать это в bashдрузьях (вы действительно можете выбить себя из колеи, используя sedили awk). Довольно простой способ — использовать cutдля получения имени папки

if [[ -d "$folder" ]]; then
    target=$(echo $(echo "$folder" | cut -d- -f 3))
    echo "Archiving $folder to Archived Projects → $target...";
    mv "$folder" /Volumes/communications/Projects/Archived\ Projects/$target/
fi

Это $(echo $(echo ... ))ленивый подход к избавлению от начального/конечного пробела (потому что он cutне поддерживает многосимвольные разделители).


Если вы хотите нокаутировать себя, sedвы можете использовать

    target=$(echo "$folder" | sed -n 's/^[^\-]*-[^\-]*- \([^\-]*\) -.*/\1/p')

вместо cut. Это работает только в том случае, если имя целевой папки не содержит самого -себя.


Вместо сопоставления с образцом вы также можете использовать функцию оболочки, чтобы инкапсулировать большую часть сложности.

#!/bin/bash

function checkAndMove() {
    if [[ "$1" == *$2* ]]; then
        echo "Archiving $1 to Archived Projects → $2...";
        mv "$1" /Volumes/communications/Projects/Archived\ Projects/$2/
    fi
}

cd /Volumes/communications/Projects/Completed\ Projects/

for folder in *; do
    if [[ -d "$folder" ]]; then
        checkAndMove Academics
        checkAndMove Admissions
        ...
    fi
done

Как насчет использования awk с опцией разделителя полей -F и разделения поля дефисом. Затем получите третье поле.

ОБНОВЛЯТЬ

Я обновил код, чтобы использовать результат, возвращенный из awk, для размещения папки назначения. Это экономит много кода. А также использовал разделитель «-», как указал Ян С в комментариях.

#!/bin/bash

# Go to the Completed Projects folder.
cd /Volumes/communications/Projects/Completed\ Projects/

# Find a folder with a specified string (e.g. "Academics") in its name.
# Move (not copy) the folder to its corresponding sub-folder of the Archived Projects folder. (e.g. /Academics)

for folder in *; do
    if [[ -d "$folder" ]]; then
        thirdfield=`echo "$folder" | /usr/bin/awk -F ' - ' '{print $3}'`;
        echo "Archiving $folder to Archived Projects → $thirdfield...";
        mv "$folder" /Volumes/communications/Projects/Archived\ Projects/"$thirdfield"/"$folder"    
    fi     
done

Я также добавил /"$folder" в конце перемещения, чтобы перемещалась сама папка. вы можете изменить это, если это не то, что вы хотите, удалив папку «$» в конце команды mv.


Вы также можете выполнить перекрестную проверку по массиву из 7 имен, поэтому будут перемещены только те папки, которые соответствуют. (вы можете вставить оператор else, где это необходимо)

#!/bin/bash

# Go to the Completed Projects folder.
cd /Volumes/communications/Projects/Completed\ Projects/

# Find a folder with a specified string (e.g. "Academics") in its name.
# Move (not copy) the folder to its corresponding sub-folder of the Archived Projects folder. (e.g. /Academics)

# Array of names to check against
ArrayName=(Academics Admissions  Alumni Communications Development President Student)

for folder in *; do
    if [[ -d "$folder" ]]; then
        thirdfield=`echo "$folder" | /usr/bin/awk -F ' - ' '{print $3}'`;

        for var in "${ArrayName[@]}"; do
            # Only move the folder if its key name exists in the arrary
            if [ "${var}" = "$thirdfield" ]; then
                echo "Archiving $folder to Archived Projects → $thirdfield...";
                mv "$folder" /Volumes/communications/Projects/Archived\ Projects/"$thirdfield"/"$folder"   
            fi
        done
    fi
done
awkопределенно путь, если это должно абсолютно оставаться в bash.
Также я бы разделился, ' - 'а не просто'-'
@IanC. Хороший вопрос, я отрегулирую это. Я на самом деле только что встал с мыслью об использовании thiredfield в качестве var в папке назначения, так что это поможет. (И я вижу, пока я спал, вы сделали именно это :-))

Если вы можете изучить bash, вы, безусловно, можете выучить лучший язык, такой как Ruby, для решения этой проблемы.

В том, что я публикую, есть много возможностей для улучшения, но вот базовый Ruby, который сделает вашу перекатегоризацию за вас. Некоторые преимущества этого кода Ruby по сравнению с вашим кодом bash:

  1. Он обрабатывает добавление новых clientполей и автоматически перемещает их в соответствии с вашей предпочтительной схемой архивирования.
  2. Он создает промежуточные каталоги, если они не существуют
  3. Он останавливается, если есть проблема с перемещением каталога, что означает, что если он не останавливается, все перемещается успешно.

И, конечно же, если вы спросите меня, это бесконечно более удобочитаемо и расширяемо. Если вы можете выучить bash, Ruby будет очень сложным, и вы обнаружите, что с его помощью вы можете автоматизировать лучше, чем с bash.

Я старался придерживаться того, как работает ваш bash, чтобы он выглядел знакомо. Как видите, это немного короче, чем тот удар.

#!/usr/bin/env ruby

require 'fileutils'

SOURCE = '/Users/ianc/tmp/ad'
DESTINATION = '/Users/ianc/tmp/ad-new'

Dir.chdir(SOURCE)

Dir['**'].each do |f|
  if File.exists?(f) && File.directory?(f)
    # Format: [YYYY.MM.DD] - Medium - Client - Project name - details--details - JobNumber
    date, medium, client, project, details, job_number = f.split(' - ', 6)
    if client
      destination = File.join(DESTINATION, client)
      FileUtils.mkpath destination if !File.exists?(destination)
      destination = File.join(destination, f)
      source = File.join(SOURCE, f)
      puts 'Moving: ' + source + ' --> ' + destination
      FileUtils.mv(source, destination)
    else
      puts 'Skipping: ' + f
    end
  end
end
Итак, вы говорите, что я могу выполнить сценарий Ruby из терминала Mac OS X так же, как сценарий bash? (Я явно не программист — пока.) И если да, то что я должен ввести в командную строку, чтобы выполнить указанный сценарий Ruby?
Сохраните это в файл, как и ваш сценарий оболочки, а затем установите для него бит выполнения, набрав: chmod +x <file name>. Теперь просто введите имя файла, и он запустится. Эта волшебная первая строка !#/usr/bin/env rubyговорит ОС запустить скрипт с использованием Ruby.
А у Мака есть Ruby из коробки? @IanC.
Да. Руби есть из коробки.
Может ли этот сценарий сломаться, если он встретит формат, отличный от описанного в #comment? Раздел details--detailsсильно различается от папки к папке. Он часто включает специальные символы, такие как [ ] -. @IanC.
Он разбивается на все экземпляры '-' -- так что ничего, пока в разделе details--detailsимени не встречается шаблон '-', он не сломается.
облом. Иногда в разделе есть «-» details--details. Должен ли я указывать это выше?