inotify-win
inotify-win copied to clipboard
Not an issue more a simplification
The whole project can be distilled down to one cmd
/* 2>nul & @echo off
echo. &echo compiling inotifywait.exe
echo using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; >"%tmp%\AssemblyInfo.cs"
echo [assembly: AssemblyTitle("https://github.com/thekid/inotify-win")] >>"%tmp%\AssemblyInfo.cs"
echo [assembly: AssemblyDescription("A port of the inotifywait tool for Windows")] >>"%tmp%\AssemblyInfo.cs"
echo [assembly: AssemblyConfiguration("")] >>"%tmp%\AssemblyInfo.cs"
echo [assembly: AssemblyCompany("Timm Friebe")] >>"%tmp%\AssemblyInfo.cs"
echo [assembly: AssemblyProduct("inotify-win")] >>"%tmp%\AssemblyInfo.cs"
echo [assembly: AssemblyCopyright("Copyright © 2012 - 2023 Timm Friebe")] >>"%tmp%\AssemblyInfo.cs"
echo [assembly: AssemblyTrademark("")] >>"%tmp%\AssemblyInfo.cs"
echo [assembly: AssemblyCulture("")] >>"%tmp%\AssemblyInfo.cs"
echo [assembly: AssemblyVersion("1.10.0.0")] >>"%tmp%\AssemblyInfo.cs"
echo [assembly: ComVisible(false)] >>"%tmp%\AssemblyInfo.cs"
echo [assembly: Guid("4254314b-ae21-4e2f-ba52-d6f3d83a86b5")] >>"%tmp%\AssemblyInfo.cs"
echo. & echo check the Assembly Info is OK & echo. & type "%tmp%\AssemblyInfo.cs" & echo. & pause
%WINDIR%\Microsoft.NET\Framework\v4.0.30319\csc.exe /nologo /t:exe /out:inotifywait.exe %~nx0 "%tmp%\AssemblyInfo.cs"
inotifywait.exe
exit /b
*/
using System;
using System.Threading;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
namespace De.Thekid.INotify
{
/// See also <a href="http://linux.die.net/man/1/inotifywait">inotifywait(1) - Linux man page</a>
public class ArgumentParser
{
/// Helper method for parser
protected string Value(string[] args, int i, string name)
{
if (i > args.Length)
{
throw new ArgumentException("Argument " + name + " requires a value");
}
return args[i];
}
/// Tokenizes "printf" format string into an array of strings
protected string[] TokenizeFormat(string arg)
{
var result = new List<string>();
var tokens = arg.Split(new char[]{ '%' });
foreach (var token in tokens)
{
if (token.Length == 0) continue;
if ("efwT".IndexOf(token[0]) != -1)
{
result.Add(token[0].ToString());
if (token.Length > 1)
{
result.Add(token.Substring(1));
}
}
else
{
result.Add(token);
}
}
return result.ToArray();
}
private void ParseArgument(string option, string[] args, ref int i, Arguments result)
{
if ("--recursive" == option || "-r" == option)
{
result.Recursive = true;
}
else if ("--monitor" == option || "-m" == option)
{
result.Monitor = true;
}
else if ("--quiet" == option || "-q" == option)
{
result.Quiet = true;
}
else if ("--event" == option || "-e" == option)
{
result.AddEvents(Value(args, ++i, "event").Split(','));
}
else if ("--format" == option)
{
result.Format = TokenizeFormat(Value(args, ++i, "format"));
}
else if ("--exclude" == option)
{
result.Exclude = new Regex(Value(args, ++i, "exclude"));
}
else if ("--excludei" == option)
{
result.Exclude = new Regex(Value(args, ++i, "exclude"), RegexOptions.IgnoreCase);
}
else if ("--include" == option)
{
result.Include = new Regex(Value(args, ++i, "include"));
}
else if ("--includei" == option)
{
result.Include = new Regex(Value(args, ++i, "include"), RegexOptions.IgnoreCase);
}
else if (option.StartsWith("--event="))
{
result.AddEvents(option.Split(new Char[]{'='}, 2)[1].Split(','));
}
else if (option.StartsWith("--format="))
{
result.Format = TokenizeFormat(option.Split(new Char[]{'='}, 2)[1]);
}
else if (option.StartsWith("--exclude="))
{
result.Exclude = new Regex(option.Split(new Char[]{'='}, 2)[1]);
}
else if (option.StartsWith("--excludei="))
{
result.Exclude = new Regex(option.Split(new Char[]{'='}, 2)[1], RegexOptions.IgnoreCase);
}
else if (option.StartsWith("--include="))
{
result.Include = new Regex(option.Split(new Char[]{'='}, 2)[1]);
}
else if (option.StartsWith("--includei="))
{
result.Include = new Regex(option.Split(new Char[]{'='}, 2)[1], RegexOptions.IgnoreCase);
}
else if (Directory.Exists(option) || File.Exists(option))
{
result.Paths.Add(System.IO.Path.GetFullPath(option));
}
else if (option.StartsWith("-"))
{
throw new ArgumentException(string.Format("Unknown option: `{0}`", option));
}
else
{
throw new ArgumentException(string.Format("Path does not exist: `{0}`", option));
}
}
/// Creates a new argument parser and parses the arguments
public Arguments Parse(string[] args)
{
var result = new Arguments();
for (var i = 0; i < args.Length; i++)
{
if (!args[i].StartsWith("--") && args[i].StartsWith("-") && args[i].Length > 2)
{
string options = args[i];
for (var j = 1; j < options.Length; j++)
{
ParseArgument("-" + options.Substring(j, 1), args, ref i, result);
}
}
else
{
ParseArgument(args[i], args, ref i, result);
}
}
return result;
}
/// Usage
public void PrintUsage(string name, TextWriter writer)
{
writer.WriteLine("Usage: " + name + " [options] path [...]");
writer.WriteLine();
writer.WriteLine("Options:");
writer.WriteLine("-r/--recursive: Recursively watch all files and subdirectories inside path");
writer.WriteLine("-m/--monitor: Keep running until killed (e.g. via Ctrl+C)");
writer.WriteLine("-q/--quiet: Do not output information about actions");
writer.WriteLine("-e/--event list: Which events (create, modify, delete, move) to watch, comma-separated. Default: all");
writer.WriteLine("--format format: Format string for output.");
writer.WriteLine("--exclude: Do not process any events whose filename matches the specified regex");
writer.WriteLine("--excludei: Ditto, case-insensitive");
writer.WriteLine("--include: Only process events whose filename matches the specified regex");
writer.WriteLine("--includei: Ditto, case-insensitive");
writer.WriteLine();
writer.WriteLine("Formats:");
writer.WriteLine("%e : Event name");
writer.WriteLine("%f : File name");
writer.WriteLine("%w : Path name");
writer.WriteLine("%T : Current date and time");
}
}
public class Arguments
{
private static string[] defaultEvents = new string[] { "create", "modify", "delete", "move" };
private List<string> events = new List<string>();
private List<string> paths = new List<string>();
private string[] format = new string[] { "w", " ", "e", " ", "f" };
/// -r
public bool Recursive { get; set; }
/// -m
public bool Monitor { get; set; }
/// -q
public bool Quiet { get; set; }
/// -f
public string[] Format {
get { return format; }
set { format = value; }
}
/// --include[i]
public Regex Include { get; set; }
/// --exclude[i]
public Regex Exclude { get; set; }
/// -e
public void AddEvents(IEnumerable<string> names)
{
foreach (var name in names)
{
if (defaultEvents.Contains(name))
{
events.Add(name);
}
else
{
throw new ArgumentException("Unknown event name '" + name + "'");
}
}
}
/// -e
public List<string> Events
{
get { return 0 == events.Count ? new List<string>(defaultEvents) : events.Distinct().ToList(); }
}
/// All arguments not starting with "-"
public List<string> Paths {
get { return paths; }
}
}
// List of possible changes
public enum Change
{
CREATE, MODIFY, DELETE, MOVED_FROM, MOVED_TO
}
/// Main class
public class Runner
{
// Mappings
protected static Dictionary<WatcherChangeTypes, Change> Changes = new Dictionary<WatcherChangeTypes, Change>();
private List<Thread> _threads = new List<Thread>();
private ManualResetEventSlim _stopMonitoringEvent;
private object _notificationReactionLock = new object();
private Arguments _args = null;
static Runner()
{
Changes[WatcherChangeTypes.Created]= Change.CREATE;
Changes[WatcherChangeTypes.Changed]= Change.MODIFY;
Changes[WatcherChangeTypes.Deleted]= Change.DELETE;
}
public Runner(Arguments args)
{
_args = args;
}
/// Callback for errors in watcher
protected void OnWatcherError(object source, ErrorEventArgs e)
{
Console.Error.WriteLine("*** {0}", e.GetException());
}
private void OnWatcherNotification(object sender, FileSystemEventArgs e)
{
var w = (FileSystemWatcher)sender;
HandleNotification((FileSystemWatcher)sender, e, () => Output(Console.Out, _args.Format, w, Changes[e.ChangeType], e.Name));
}
private void OnRenameNotification(object sender, RenamedEventArgs e)
{
var w = (FileSystemWatcher)sender;
HandleNotification(w, e, () =>
{
Output(Console.Out, _args.Format, w, Change.MOVED_FROM, e.OldName);
Output(Console.Out, _args.Format, w, Change.MOVED_TO, e.Name);
});
}
private void HandleNotification(FileSystemWatcher sender, FileSystemEventArgs e, Action outputAction)
{
// Lock so we don't output more than one change if we were only supposed to watch for one.
// And to serialize access to the console
lock (_notificationReactionLock)
{
// if only looking for one change and another thread beat us to it, return
if (!_args.Monitor && _stopMonitoringEvent.IsSet)
{
return;
}
if (
(null != _args.Exclude && _args.Exclude.IsMatch(e.FullPath)) ||
(null != _args.Include && !_args.Include.IsMatch(e.FullPath))
)
{
return;
}
outputAction();
// If only looking for one change, signal to stop
if (!_args.Monitor)
{
_stopMonitoringEvent.Set();
}
}
}
/// Output method
protected void Output(TextWriter writer, string[] tokens, FileSystemWatcher source, Change type, string name)
{
foreach (var token in tokens)
{
var path = Path.Combine(source.Path, name);
switch (token[0])
{
case 'e':
writer.Write(type);
if (Directory.Exists(path))
{
writer.Write(",ISDIR");
}
break;
case 'f': writer.Write(Path.GetFileName(path)); break;
case 'w': writer.Write(Path.Combine(source.Path, Path.GetDirectoryName(path))); break;
case 'T': writer.Write(DateTime.Now); break;
default: writer.Write(token); break;
}
}
writer.WriteLine();
}
public void Processor(object data)
{
string path = (string)data;
string fileName = "*.*";
if (File.Exists(path))
{
fileName = Path.GetFileName(path);
path = Path.GetDirectoryName(path);
}
using (var w = new FileSystemWatcher {
Path = path,
IncludeSubdirectories = _args.Recursive,
Filter = fileName
}) {
w.Error += new ErrorEventHandler(OnWatcherError);
// Parse "events" argument
WatcherChangeTypes changes = 0;
if (_args.Events.Contains("create"))
{
changes |= WatcherChangeTypes.Created;
w.Created += new FileSystemEventHandler(OnWatcherNotification);
}
if (_args.Events.Contains("modify"))
{
changes |= WatcherChangeTypes.Changed;
w.Changed += new FileSystemEventHandler(OnWatcherNotification);
}
if (_args.Events.Contains("delete"))
{
changes |= WatcherChangeTypes.Deleted;
w.Deleted += new FileSystemEventHandler(OnWatcherNotification);
}
if (_args.Events.Contains("move"))
{
changes |= WatcherChangeTypes.Renamed;
w.Renamed += new RenamedEventHandler(OnRenameNotification);
}
// Main loop
if (!_args.Quiet)
{
Console.Error.WriteLine(
"===> {0} {1}{2}{4} for {3}",
_args.Monitor ? "Monitoring" : "Watching",
path,
_args.Recursive ? " -r" : "",
String.Join(", ", _args.Events),
fileName
);
}
w.EnableRaisingEvents = true;
_stopMonitoringEvent.Wait();
}
}
/// Entry point
public int Run()
{
using (_stopMonitoringEvent = new ManualResetEventSlim(initialState: false))
{
Console.CancelKeyPress += delegate { _stopMonitoringEvent.Set(); };
foreach (var path in _args.Paths)
{
var t = new Thread(new ParameterizedThreadStart(Processor));
t.Start(path);
_threads.Add(t);
}
_stopMonitoringEvent.Wait();
foreach (var thread in _threads)
{
if (thread.IsAlive) thread.Abort();
thread.Join();
}
return 0;
}
}
/// Entry point method
public static int Main(string[] args)
{
var p = new ArgumentParser();
// Show usage if no args or standard "help" args are given
if (0 == args.Length || args[0].Equals("-?") || args[0].Equals("--help"))
{
p.PrintUsage("inotifywait", Console.Error);
return 1;
}
// Run!
return new Runner(p.Parse(args)).Run();
}
}
}