#!/usr/bin/env ruby # diru is a tool for moving around in the users directory # structure. diru provides input for the shell cd command for entering # a new directory. However some commands do not produce input for cd. # # diru has hub (daemon) which creates new service (server) threads for # diru clients. Before user can start using diru, a server should be # created. # # diru has server side and client side. The server maintains # information about users directories and also the state of the # queries issued by the client. Client is what the end user uses. # # diru client command has to be integrated to the currently used # shell in order to force the shell to change the current # directory. Lets assume that the shell alias/function that user is # actually using is "dr" for now. If diru returns success, then the # diru output should be used as new directory. If diru status is # failure, then directory should not be changed. # # Example of diru use: # shell> dr / build src # # This command means that the client sends to the server a request for # directory search. The search starts from Diru root ("/") and the # rest of the path should include the words "build", and # "src". If path with this spec is found, the shell directory gets # updated. # # Perform: # shell> dr @ doc # or # shell> dr i # # for some documentation. # # Diru setup: # # Start hub (deamon) # * shell> diru --hub # # Open server for client. # * shell> diru -s # # Use client # * shell> diru -p 41115 -c r # # # Feature list: # * User root dir (downwards) search # * Current directory (downwards) dir search # * Bookmarking directories # * Left-over search with multiple matches # * Fast jump to specific common directories # * Peer dir jump # * Server state reset # * Revert mode, unknown commands to cd # require 'como' include Como require 'drb' require 'yaml' require 'fileutils' # require 'byebug' Spec.command( 'diru', 'Tero Isannainen', '2017', [ [ :switch, 'hub', nil, "H: Start hub." ], [ :opt_single, 'hport', nil, "H: Hub port (default: DIRU_HUB_PORT or 41114)." ], [ :switch, 'hkill', nil, "H: Kill hub." ], [ :switch, 'nodaemon', nil, "H: No daemon for server." ], [ :opt_any, 'server', '-s', "S: Open server for user (default: $USER)." ], [ :opt_single, 'kill', '-k', "S: Close server." ], [ :switch, 'list', '-l', "S: List servers." ], [ :opt_single, 'options', '-o', "SC: Options File." ], [ :opt_single, 'port', '-p', "SC: Server port for client (default: DIRU_PORT or ~/.diru.prt)." ], [ :switch, 'change', '-c', "C: Change dir (i.e. target is to change dir)." ], [ :exclusive, 'template', '-t', "C: Display Options File template." ], [ :exclusive, 'cmddoc', '-d', "C: Display command documentation." ], [ :default, nil, nil, "C: Client commands (try: diru i)." ], ] ) # ------------------------------------------------------------ # Common: # ------------------------------------------------------------ # Diru common features. module Diru # Default port. DIRU_HUB_PORT = ENV['DIRU_HUB_PORT'] || 41114 # Load yaml configuration file (if exists). def Diru.load_conf( conf_file ) conf = nil if File.exist?( conf_file ) begin conf = YAML.load( File.read( conf_file ) ) rescue STDERR.puts "Diru Error: Broken YAML, please fix: \"#{conf_file}\"..." end end conf end # See: Diru.load_conf def load_conf( conf_file ) @conf = Diru.load_conf( conf_file ) end # Error message display. def Diru.error( msg ) STDERR.puts "Diru Error: #{msg}" exit( false ) end # Warning message display. def Diru.warn( msg ) STDERR.puts "Diru Warning: #{msg}" end # List the dir content (default: pwd). def Diru.list( dir = '.', glob = '*' ) Dir.glob( "#{dir}/#{glob}" ) end # List the dir files (default: pwd). def Diru.list_files( dir = '.', glob = '*' ) Diru.list( dir, glob ).select do |i| File.file?(i) == true end end # Find file entry from directory hierarchy upwards. def Diru.find_upper_file( file, dir = Dir.pwd ) found = Diru.list_files( dir, file ) if found.empty? dir = File.dirname( dir ) if dir == "/" raise RuntimeError, "Could not find file \"#{file}\"!" else return Diru.find_upper_file( file, dir ) end else return found[0] end end # Find file entry from directory hierarchy upwards, exit if not found. def Diru.must_find_upper_file( file, dir = Dir.pwd ) begin Diru.find_upper_file( file, dir ) rescue RuntimeError STDERR.puts "Could not find file \"#{file}\"!" exit( false ) end end def Diru.get_opts_file( file = nil ) opts_file = nil if Opt['options'].given opts_file = Opt['options'].value elsif file opts_file = file elsif ENV['DIRU_OPTS'] opts_file = ENV['DIRU_OPTS'] else opts_file = "#{ENV['HOME']}/.diru.yml" end opts_file end end # Diru Server State. class Search include Diru def initialize( root, user, opts_file = nil ) # Server root. @root = root if @root == '/' @rootpart = '' else @rootpart = @root end # Change directory to DIRU root. Dir.chdir( @root ) # User of server. @user = user # List of old matches. @old = [] # Numbered bookmarks. @book = [] # Numbered history. @hist = [] # Favorites. @fav = {} # History limit. @histlimit = 20 # Sync period for options (and data if no @dsync). @sync = 10 # Sync period for data. @dsync = nil # Options File. @opts_file = opts_file # Temporary storage (scratch pad). @scratch = {} # Glob pattern for DB build. @glob = "**/*" @logging = false # if Opt['log'].given # @logging = true # end # Data update thread. @th_data = nil # Options update thread. @th_opts = nil # Lock for DB access. @datalock = Mutex.new @optslock = Mutex.new # Initial data update. update_conf update_data start_th_data start_th_opts end # Return opts_file. def opts_file @opts_file end # Return root. def root @root end # Return user. def user @user end # Add bookmark (if not already). def abook( dir ) idx = @book.index dir unless idx @book.unshift dir end end # Delete bookmark. def dbook( idx ) @book.delete_at idx end # Get bookmark. def gbook( idx ) @book[ idx ] end # Get all bookmarks. def book @book end # Reset bookmarks. def rbook @book = [] end # Save bookmarks to file. def savebook( file ) File.write( file, @book.join("\n") + "\n" ) end # Load bookmarks from file. def loadbook( file ) @book = File.read( file ).split("\n") end # Add history. def ahist( dir ) if dir != @hist[0] if @hist.length >= @histlimit @hist = [ dir ] + @hist[ 0..(@histlimit-2) ] else @hist = [ dir ] + @hist end end end # Get history. def ghist( idx ) @hist[ idx ] end # Get all historys. def hist @hist end # Reset history. def rhist @hist = [] end # Get (and update) favorite. def gfav( tag ) "#{@rootpart}/#{@fav[ tag ]}" end # Return the whole fav. def fav @fav end # Update options. def update_conf if @opts_file return unless load_conf( @opts_file ) if @conf[:dsync] && @conf[:dsync] >= 0 @dsync = @conf[:dsync] end if @conf[:sync] && @conf[:sync] >= 1 @sync = @conf[:sync] @dsync = @sync unless @dsync end if @dsync == 0 kill_th_data else unless @th_data start_th_data end end if @conf[:hist] && ( @conf[:hist] >= 2 ) @histlimit = @conf[:hist] else @histlimit = 20 end if @conf[ :favs ] @fav = @conf[ :favs ] end end end # Set scratch dir. def settmp( dir, reg = nil ) @scratch[ reg ] = dir end # Get scratch dir. def gettmp( reg = nil ) @scratch[ reg ] end # Reset scratch dir. def rtmp @scratch = {} end # Get all scratch regs. def scratch @scratch end def abs2rel( path ) if path == @root '' else rem = "#{@rootpart}/" rel = path.dup rel[rem] = '' rel end end # Match dir/pattern with glob (fnmatch) from DB. def match( dir, pattern ) list = [] if dir begin dir = abs2rel( dir ) rescue return [] end r = dir + '*' + pattern else r = pattern end @datalock.synchronize do @data.each do |i| if File.fnmatch( r, i ) list.push "#{@rootpart}/#{i}" end end end # Update old list. @old = list.rotate list end # Match dir/pattern with regex from DB. def rematch( dir, pattern ) list = [] if dir begin dir = abs2rel( dir ) rescue return [] end r = dir + '.*' + pattern else r = pattern end @datalock.synchronize do @data.each do |i| if i.match( r ) list.push "#{@rootpart}/#{i}" end end end # Update old list. @old = list.rotate list end # Get next match from old. def next ret = @old[0] @old.rotate! ret end # Make directory. def mk_dir( dir ) rel = abs2rel( dir ) unless @data.index( rel ) @data = @data.push( rel ).sort FileUtils.mkdir_p dir end end # Make directory in sync. def mk_dir_sync( dir ) @datalock.synchronize do mk_dir dir end end # Remove directory. def rm_dir( dir ) rel = abs2rel( dir ) if @data.index( rel ) @data.delete( rel ) FileUtils.rmdir dir end end # Remove directory in sync. def rm_dir_sync( dir ) @datalock.synchronize do rm_dir dir end end # Update directory DB. # # Relative dir data under Project root. # test # test/dir_0 # ... def update_data @data = Dir.glob( @glob ).select{|ent| File.directory?( ent )} @data = @data.sort end # Update DB with MT sync. def update_data_sync @datalock.synchronize do update_data end end # Update DB with MT sync. def update_opts_sync @optslock.synchronize do update_conf end end # Kill search and cleanup resources. def kill kill_th_data kill_th_opts end # Reset dynamic state. def reset log "reset" @old = [] @book = [] @hist = [] update_data_sync end # Start data thread. def start_th_data unless @dsync == 0 @th_data = Thread.new do loop do # puts "data update..." sleep @dsync update_data_sync end end end end # Kill data thread. def kill_th_data Thread.kill( @th_data ) if @th_data @th_data = nil end # Start opts thread. def start_th_opts @th_opts = Thread.new do loop do sleep @sync update_opts_sync end end end # Kill opts thread. def kill_th_opts Thread.kill( @th_opts ) if @th_opts @th_opts = nil end # Enable logging. def logon @logging = true end # Disable logging. def logoff @logging = false end # Log events. def log( msg ) if @logging fh = File.open( "#{ENV['HOME']}/.diru.log", "a" ) fh.puts "#{Time.timestamp()}: #{msg}" fh.close end end end # ------------------------------------------------------------ # Server program. # ------------------------------------------------------------ # Diru hub. class Hub # Start in daemon or direct mode. def Hub.start( port, daemon = true ) if daemon p = fork do begin Hub.new( port ) rescue Diru.error "Could not start Hub!" exit( false ) end end Process.detach( p ) else begin Hub.new( port ) rescue Diru.error "Could not start Hub!" exit( false ) end end end # Server collection. @@servers = {} attr_reader :port def initialize( port ) @port = port # Start the service @hub = DRb.start_service( "druby://localhost:#{@port}", self ) DRb.thread.join end # Kill hub. def kill @th = Thread.new do STDERR.puts "Diru Hub: Exiting sooooon..." sleep 3 kill_servers sleep 3 STDERR.puts "Diru Hub: Exit done..." @hub.stop_service exit( false ) end end # Create new server and return port for the server. def get_server( root, user, opts_file ) 50.times do |i| if @@servers[ @port+1+i ] == nil port = @port+1+i s = Service.new( root, user, port, opts_file ) @@servers[ port ] = s return port end end 0 end # List all server ports. def list_servers @@servers.keys.map{ |i| [ i, @@servers[i].root, @@servers[i].user ] } end # Kill all servers. def kill_servers @@servers.keys.each do |s| kill_server( s ) end @@servers = {} end # Kill server with the given port. def kill_server( s_port ) s = @@servers[ s_port ] if s s.kill @@servers.delete( s_port ) else nil end end end # diru client service (server) thread. class Service attr_reader :root attr_reader :user # Initialize and start service. def initialize( root, user, port, opts_file ) @root = root @user = user @port = port @drb = nil @opts_file = opts_file start self end # Kill service. def kill @search.kill Thread.kill( @th ) @drb.stop_service end # Start service. def start @th = Thread.new do # Create "front" object. @search = Search.new( @root, @user, @opts_file ) # Start the service @drb = DRb::DRbServer.new( "druby://localhost:#{@port}", @search ) @drb.thread.join end end end if Opt['template'].given puts %q{--- :hist: 20 :sync: 10 :dsync: 0 :favs: f: dir_0/dir_0_4/dir_0_4_0 g: dir_1/dir_1_2 :peers: - - "(.*/dir_0/.*)/dir_0_2_0" - "\\1" - - "(.*/dir_0/.*)/dir_0_1_0" - "\\1/dir_0_1_1" } exit( false ) end # Keep in sync with README. if Opt['cmddoc'].given STDERR.puts %q{ Search commands: * "dr r" - change to Project root dir. * "dr r