Monkey Joe will do the monkey job for you.
JavaScript Shell
Switch branches/tags
Nothing to show
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
bin
lib
INSTALL.md
README.md
USAGE
VERSION
package.json

README.md

Monkey Joe

Monkey Joe — инструмент автоматического запуска команд при появлении новых файлов, изменении или удалении существующих в директориях, за которыми "следит" Monkey Joe (далее mjoe).

ВНИМАНИЕ! MONKEY JOE РАБОТАЕТ ТОЛЬКО В LINUX!

В упрощённом виде mjoe работает так:

  • файловая система инициирует событие (файл изменился, удалился, добавился и т.п.);
  • mjoe получает уведомление об этом событии (event);
  • mjoe пересылает событие каждому обработчику (worker), описанному в конфигурационном файле;
  • специальная функция (matcher) обработчика решает, следует ли отреагировать на событие;
  • если требуется реакция, вызываются пользовательские функции (callbacks).

Пример

~$ mkdir test && cd test
~/test$ echo foo > dummy.js
~/test$ cat > .mjoe.js
exports.config = {
    exclude: ['*.git'],
    workers: [
        {
            matcher: '*.js',
            callbacks: function(e) {
                console.log(e);
            }
        }
    ]
};
^C
~/test$ mjoe
[2011.07.07 22:15:56.117] Monkey Joe запускается...
[2011.07.07 22:15:56.119] Monkey Joe запустился

В терминале открываем новую сессию и редактируем dummy.js:

~/test$ echo bar >> dummy.js

Monkey Joe должен вывести в терминал что-то типа:

{ type: 'ninotify',
  mask: 4,
  path: '/home/nikita-vasilyev/test/foo.js' }

Предварительные требования:

Установка

Для установки mjoe на Linux следует выполнить команды:

npm install mjoe
npm install ninotify

Если установка прошла без ошибок, никаких дополнительных действий совершать не надо, всё на своих местах и mjoe доступен по команде mjoe.

Обновление

Для обновления mjoe на Linux достаточно выполнить команды:

npm update mjoe
npm update ninotify

Удаление

Для удаления mjoe и служебных модулей следует выполнить команды:

npm uninstall mjoe
npm uninstall fswatch
npm uninstall yanlibs
npm uninstall ninotify

Запуск

Запуск следует производить в директории, за содержимым которой (включая поддиректории) должен следить mjoe. При этом на том же уровне должен находиться конфигурационный файл (далее конфиг) .mjoe.js. Например, чтобы mjoe корректно запустился в директории foo и обрабатывал изменения в foo, bar и bbr, структура директорий и местонахождение конфига должны быть такими:

/foo
   .mjoe.js
   /bar
   /bbr

Для запуска mjoe следует выполнить команду:

mjoe

Если требуется узнать версию, запускать следует так:

mjoe --version

Запуск в режиме логирования:

mjoe -vv

Вывод справки:

mjoe --help

Следует понимать, что mjoe отслеживает события только после запуска. Если вы изменили файл, а затем запустили mjoe, ничего не произойдёт. Говоря формально, mjoe является stateless, а не stateful, т.е. между запусками состояние (информация о событиях, зеркало файловой системы и т.п.) не сохраняется.

Формат конфига

Конфигурационный файл mjoe — программа на языке JavaScript. И если для добавления элементарных функций-обработчиков достаточно базовых знаний, то для написания более сложной функциональности умение писать на JavaScript потребуется в полной мере.

Конфиг состоит из трёх частей (настройки, exclude и workers):

exports.config = {
    depth: ...,
    interval: ...,
    preprocessor: ...,
    exclude: [...],
    workers: [...]
};

Depth

Параметр depth задаёт лимит вложенности директорий, начиная от местоположения конфига. Если этот лимит превышен, mjoe сообщает об ошибке и завершает работу. Обычно превышение лимита свидетельствует о наличии рекурсивных симлинок, которые следует убрать.

Значение по умолчанию, которое применяется при отсутствии параметра в конфиге: 20.

Interval

Параметр interval задаёт временной промежуток (в миллисекундах) между запусками workers. Иными словами, если два события пришли с разницей в одну миллисекунду, mjoe сначала запустит workers на первое событие, а на второе событие запустит лишь спустя указанный интервал. Это позволяет избавиться от конфликтов, когда workers в процессе работы обращаются к одним и тем же файлам. Таким образом, интервал следует подбирать с учётом наиболее продолжительно работающего workerа. Если в конфиге используются синхронизированные exec (см. API.exec), этот параметр можно не принимать во внимание.

Значение по умолчанию, которое применяется при отсутствии параметра в конфиге: 1000.

Preprocessor

Параметр preprocessor задаёт функцию, которая обрабатывает событие до его передачи в workers. Иначе говоря, если данный конфиг нуждается в дополнении события или в иной структуре объекта, передаваемого в workers, следует добавить в конфиг функцию preprocessor, результат выполнения которой отправится в workers.

Exclude

Параметр exclude позволяет указать mjoe те директории, которые не надо включать в обработку. Формат параметра идентичен формату workers.matcher, о чём следует читать соответствующую главу.

При инициализации mjoe совершает обход дерева директорий, проверяя каждую через exclude. Если директория исключена из обработки, соответственно, исключаются все её поддиректории.

Также на вхождение в exclude проверяются и директории, появляющиеся в процессе работы mjoe.

Workers

В массиве workers перечисляются обработчики событий:

exports.config = {
    ...
    workers: [
        {
            matcher: ...,
            callbacks: ...
        },
        {
            matcher: ...,
            callbacks: ...
        }
    ]
};

Обработчик событий — объект, функции которого получают событие, проверяют (за это отвечает matcher), требуется ли реакция, и в случае положительного ответа реагируют на событие — запускают callbacks.

matcher

Matcher — это RegExp, функция, строка с wildcard или массив, включающий в себя перечисленные варианты. Задачей matcher является определение того, вызывать ли обработчики по возникшему событию, результатом работы должно быть булево значение (true или false).

Для RegExp и wildcard сравнение всегда происходит со строкой, значением которой является полный путь к файлу. В случае с функцией можно (и нужно) использовать свойства объекта-события.

RegExp:

Достаточно простой и гибкий способ задать matcher:

exports.config = {
    ...
    workers: [
        {
            matcher: /\.txt$/,
            callbacks: [...]
        }
    ]
};

В этом варианте mjoe вызовет метод test() у регулярного выражения. Фактически, при входящем пути /home/afelix/sample/file.txt произойдёт следующий вызов:

/\.txt$/.test('/home/afelix/sample/file.txt')

что вернёт true и потому будут вызваны пользовательские функции.

Функция:

Более сложный, но более функциональный способ задать matcher:

exports.config = {
    ...
    workers: [
        {
            matcher: function(s, e) { return /\.txt$/.test(e.path) },
            callbacks: [...]
        }
    ]
};

В этом варианте для проверки будет вызвана анонимная функция и если входящий путь к файлу завершается на .txt, будут вызваны пользовательские функции.

Два аргумента у функции служат разным целям:

  • s — техническая строка вида 'ninotify 512 /test/file.txt' и она нужна в очень-очень редких случаях, используйте следующий аргумент.
  • e — объект события, содержащий как разобранные на составляющие атрибуты события, так и дополнительные свойства, если был задействован preprocessor; более детально об этом объекте написано в разделе Пользовательские функции (callbacks).

Строка с wildcard

Наконец, наиболее простой способ, шаблон задаётся в принятом в командной оболочке Unix формате:

  • * — все символы.
  • ? — один символ.
  • [последовательность] — любой символ последовательности.
  • [!последовательность] — любой символ вне последовательности.

Пример:

exports.config = {
    ...
    workers: [
        {
            matcher: '*.txt',
            callbacks: [...]
        }
    ]
};

Этот matcher сработает на всех файлах с расширением .txt.

callbacks

Callbacks — пользовательские функции, вызываемые для обработки событий. Можно указывать как одну функцию:

exports.config = {
    ...
    workers: [
        {
            matcher: ...,
            callbacks: echo
        }
    ]
};

так и несколько, перечисляя через запятую:

exports.config = {
    ...
    workers: [
        {
            matcher: ...,
            callbacks: [echo, test, doSomething]
        }
    ]
};

Пользовательские функции (callbacks)

Функции, который вызываются после того, как соответствующий matcher определил, что событие следует обработать. Сигнатура функции такова:

function name(event) { ... }

где event — объект следующей структуры:

event {
    type: ...,
    mask: ...,
    path: ...
}

type — тип события. Строка, которая может потребоваться обработчику в особых случаях, но в подавляющем большинстве случаев не требуется. Например, у событий, порождаемых модулем ninotify, типом будет ninotify.

mask — числовая маска события. На данный момент в ней кодируются четыре события:

E_CREATE = 1,        // файл создан
E_DELETE = 2,        // файл удалён
E_MODIFY = 4,        // файл изменился
E_STATS_CHANGED = 8; // изменились атрибуты файла

использовать которые можно так:

var mjoe = global.mjoe;
..
function test(e) {
    if (e.mask & mjoe.E_MODIFY) {
        ...
    }
};

path — полный путь к директории / файлу, с которым произошло событие.

Следует помнить о том, что функция preprocessor может изменить объект любым способом, но это уже на совести автора конфигурационного файла.

API

Для использования в конфигах mjoe предоставляет API — объект, который находится в global Node.js и подключается вот так:

var myFavoriteVariableName = global.mjoe;

Обязательным к использованию этот API не является, но на практике без него не обойтись.

pwd

Путь к директории, в которой находится конфиг mjoe.

exec

Часто возникает задача выполнить какую-нибудь командную строку. Для этих целей в Node.js есть функция exec(): принимает аргументом командную строку и выполняет её. Проблема в том, что это асинхронная функция — отдаёт управление и затем начинает выполнять команду.

Вот конфиг, использующий стандартный exec:

var exec = require('child_process').exec,
    print = require('sys').print;

exports.config = {
    workers: [{ matcher: '*', callbacks: [echo0, echo1] }]
};

function echo0() {
    console.log('start echo0');
    exec('echo 0 && sleep 5 && echo 1', ecb);
    console.log('finish echo0');
}

function echo1() {
    console.log('start echo1');
    exec('echo 2 && sleep 2 && echo 3', ecb);
    console.log('finish echo1');
}

function ecb(error, stdout, stderr) {
    print(stdout);
    print(stderr);
    if (error) print('error: ' + error);
}

Когда произойдёт событие, mjoe выполнит echo0 и echo1, вывод в консоли окажется таким:

start echo0
finish echo0
start echo1
finish echo1
2
3
0
1

Достаточно представить вместо echo0 долго работающий make и вместо echo1 быстрый make, одновременно меняющие одни и те же файлы, чтобы подумать о нужде последовательного выполнения. Потому mjoe предлагает синхронный exec:

var exec = global.mjoe.exec;

exports.config = {
    workers: [{ matcher: '*', callbacks: [echo0, echo1] }]
};

function echo0() {
    console.log('start echo0');
    exec('echo 0 && sleep 5 && echo 1');
    console.log('finish echo0');
}

function echo1() {
    console.log('start echo1');
    exec('echo 2 && sleep 2 && echo 3');
    console.log('finish echo1');
}

Вывод в консоли при событии:

start echo0
finish echo0
0
1
start echo1
finish echo1
2
3

Как видно, эта версия exec также задерживает выполнение следующей функции, что даёт дополнительную защиту от одновременных запусков.

Функция exec(path, [skipEvents = false]) принимает следующие аргументы:

  • path — командная строка, которую следует выполнить.
  • skipEvents — игнорировать ли во время выполнения командной строки события от файловой системы? по умолчанию mjoe события принимает и ставит в очередь на обработку; у вас должны быть веские причины для того, чтобы использовать exec(path, true).

E_CREATE

Событие файл создан. Использование: if (mask & mjoe.E_CREATE) ..

E_DELETE

Событие файл удалён. Использование: if (mask & mjoe.E_DELETE) ..

E_MODIFY

Событие файл изменён. Использование: if (mask & mjoe.E_MODIFY) ..

E_STATS_CHANGED

Событие атрибуты файла изменились. Использование: if (mask & mjoe.E_STATS_CHANGED) ..

Примеры конфигурационного файла

Минимальный

В matcher используется wildcard (любые символы), а в callbacks функция (вывести событие в консоль):

exports.config = {
    workers: [
        {
            matcher: '*',
            callbacks: function(e) { console.log(e) }
        }
    ]
};

При запуске в директории /home/afelix/sample/ и при добавлении файла test.txt в консоль логируется объект события:

{ type: 'ninotify',
  mask: 1,
  path: '/home/afelix/sample/test.txt' }

RegExp matcher

В Минимальном конфиге matcher изменён на регулярное выражение вместо wildcard. Однако разница также и в том, что теперь callbacks будет вызываться только для файлов с расширением .txt:

exports.config = {
    workers: [
        {
            matcher: /\.txt$/,
            callbacks: function(e) { console.log(e) }
        }
    ]
};

При запуске в директории /home/afelix/sample/ и при добавлении файла test.txt поведение идентично Минимальному конфигу — в консоль логируется объект события:

{ type: 'ninotify',
  mask: 1,
  path: '/home/afelix/sample/test.txt' }

Function matcher 1

В Минимальном конфиге matcher изменён на функцию вместо wildcard. Однако разница также и в том, что теперь callbacks будет вызываться только для файла test.txt:

exports.config = {
    workers: [
        {
            matcher: function(s) { return /test\.txt$/.test(s) },
            callbacks: function(e) { console.log(e) }
        }
    ]
};

При запуске в директории /home/afelix/sample/ и при добавлении файла test.txt поведение идентично Минимальному конфигу — в консоль логируется объект события:

{ type: 'ninotify',
  mask: 1,
  path: '/home/afelix/sample/test.txt' }

Function matcher 2

Очевидно, технического вида строка в качестве аргумента функции matcher удобна далеко не всегда: ninotify 256 /home/afelix/sample/test.txt. Потому рекомендуется использовать второй аргумент: объект-событие, в котором информация о событии разобрана на составляющие.

В конфиге Function matcher 1 в функцию matcher добавлено использование второго аргумента, в остальном поведение идентично конфигу Function matcher 1:

exports.config = {
    workers: [
        {
            matcher: function(s, e) { return /test\.txt$/.test(e.path) },
            callbacks: function(e) { console.log(e) }
        }
    ]
};

Function matcher 3

Чтобы не загромождать workers исходным кодом, можно воспользоваться удобством JavaScript и вынести функции из workers.

В конфиге Function matcher 2 функции matcher и callbacks получили имена и вынесены за пределы workers, в остальном поведение идентично конфигу Function matcher 2:

exports.config = {
    workers: [
        {
            matcher: m_log,
            callbacks: c_log
        }
    ]
};

function m_log(s, e) {
    return /test\.txt$/.test(e.path);
}

function c_log(e) {
    console.log(e);
}

Массивы матчеров и пользовательских функций

В некоторых случаях оказывается удобным перечислять в массиве матчеры разного типа, а также пользовательские функции. Этот конфиг демонстрирует весь спектр возможных типов в подобных перечислениях:

exports.config = {
    workers: [
        {
            matcher: [
                '*.css',
                /\.js$/,
                function(s, e) { return /\.txt$/.test(e.path) },
                m_log
            ],
            callbacks: [
                c_log,
                function(e) { console.log('second') }
            ]
        }
    ]
};

function m_log(s, e) {
    return /\.html$/.test(e.path);
}

function c_log(e) {
    console.log('first');
}

При событии на файлах с расширениями .css, .js, .txt или .html вывод в консоль окажется таким:

first
second

Preprocessor

Минимальный конфиг дополнен подключением mjoe-API (global.mjoe), из которого используется путь к конфигу, а также добавлена функция preprocessor: rpath. Она дополняет объект события путём к файлу относительно директории, в которой находится конфиг:

var mjoe = global.mjoe;

exports.config = {
    preprocessor: rpath,
    workers: [
        {
            matcher: '*',
            callbacks: function(e) { console.log(e) }
        }
    ]
};

function rpath(e) {
    e.rpath = e.path.substring(mjoe.pwd.length + 1);
    return e;
}

При запуске в директории /home/afelix/sample/ и при добавлении файла test.txt в консоль логируется объект события, дополненный функцией preprocessor:

{ type: 'ninotify',
  mask: 1,
  path: '/home/afelix/sample/test.txt',
  rpath: 'test.txt' }

События

Нередко требуется реагировать не только на определённые файлы, но также и отличать события, произошедшие с этими файлами. Для этого используются имена событий.

В этом конфиге подключается mjoe-API (global.mjoe) с именами событий, а также расширена функция matcher. Обратите внимание на то, что события определяются не сравнением ===, но бинарным &:

var mjoe = global.mjoe;

exports.config = {
    workers: [
        {
            matcher: m_log,
            callbacks: c_log
        }
    ]
};

function m_log(s, e) {
    return /test\.txt$/.test(e.path) &&
           (e.mask & mjoe.E_MODIFY || e.mask & mjoe.E_CREATE);
}

function c_log(e) {
    console.log(e);
}

При запуске в директории /home/afelix/sample/ и только при изменении или добавлении файла test.txt в консоль логируется объект события:

{ type: 'ninotify',
  mask: 1,
  path: '/home/afelix/sample/test.txt' }

Exclude

В типовом проекте некоторые директории не нуждаются в обработке, например, .svn.

В этом конфиге директория foo исключается из дальнейшей обработки:

exports.config = {
    exclude: '*foo',
    workers: [
        {
            matcher: '*',
            callbacks: function(e) { console.log(e) }
        }
    ]
};

Если во время работы mjoe в foo что-либо произойдёт, mjoe никак не отреагирует, даже события оттуда не получит.