├── .gitignore ├── Gemfile ├── History.md ├── LICENSE ├── README.md ├── Rakefile ├── bin └── lunchy ├── extras ├── lunchy-completion.bash └── lunchy-completion.zsh ├── lib └── lunchy.rb └── lunchy.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /test/tmp/ 9 | /test/version_tmp/ 10 | /tmp/ 11 | 12 | ## Specific to RubyMotion: 13 | .dat* 14 | .repl_history 15 | build/ 16 | 17 | ## Documentation cache and generated files: 18 | /.yardoc/ 19 | /_yardoc/ 20 | /doc/ 21 | /rdoc/ 22 | 23 | ## Environment normalisation: 24 | /.bundle/ 25 | /lib/bundler/man/ 26 | 27 | # for a library or gem, you might want to ignore these files since the code is 28 | # intended to run in multiple environments; otherwise, check them in: 29 | Gemfile.lock 30 | # .ruby-version 31 | # .ruby-gemset 32 | 33 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 34 | .rvmrc 35 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in lunchy.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | Changes 2 | ================ 3 | 4 | 0.10.4 5 | ---- 6 | 7 | - Fix autoload flag on zsh via [#67](https://github.com/eddiezane/lunchy/pull/67) 8 | 9 | 0.10.3 10 | ---- 11 | 12 | - Fix regex for exact matching 13 | 14 | 0.10.1 15 | ---- 16 | 17 | - Fix typo in bash script via [#54](https://github.com/eddiezane/lunchy/pull/54) 18 | 19 | 20 | 0.10.0 21 | ---- 22 | 23 | - Add bash completion - [bunnymatic](https://github.com/bunnymatic) [alexeyshockov](https://github.com/alexeyshockov) via [#53](https://github.com/eddiezane/lunchy/pull/53) 24 | 25 | 0.9.0 26 | ---- 27 | 28 | - Add flag for exact match - [bunnymatic](https://github.com/bunnymatic) via [#51](https://github.com/eddiezane/lunchy/pull/51) 29 | 30 | 0.8.0 31 | ---- 32 | 33 | - Add 'uninstall' command to remove installed plist 34 | - Add '-s, --symlink' option to symlink on install 35 | - Add Rakefile for development 36 | - Make clear that license is MIT, thanks [bf4!](https://github.com/bf4) 37 | - Ignore case on ls, thanks [jpcirrus!](https://github.com/jpcirrus) 38 | 39 | 0.7.0 40 | ---- 41 | 42 | - Add 'show' command to display contents of matching plist file (jonpierce) 43 | - Add '-l' option on 'ls' command to display absolute paths to plist files (jonpierce) 44 | 45 | 0.6.0 46 | ---- 47 | 48 | - Fix 'regular expression too big' (jmazzi) 49 | - Allow to force start disabled agents (koraktor) 50 | 51 | 0.5.0 52 | ----- 53 | 54 | - Add default output to start and stop (joncooper) 55 | - Add 'edit' command to edit the matching plist file (AndreyChernyh) 56 | - Allow management of daemons in /System/Library/LaunchDaemons when lunchy is run as root (fhemberger) 57 | 58 | 0.4.0 59 | ----- 60 | 61 | - Fix install on 1.8.7 (marshally) 62 | - Allow management of daemons in /Library/LaunchDaemons when lunchy is run as root 63 | 64 | 0.3.0 65 | ----- 66 | 67 | - New 'install' command to install new plist files (spagalloco) 68 | - Support persistent start/stop 69 | - Fix Ruby 1.8 issues, thanks tmm1! 70 | 71 | 0.2.0 72 | ----- 73 | 74 | - Only show agents with plists by default (fhemberger) 75 | - Warn and stop if pattern matches multiple agents (andyjeffries) 76 | - Add 'list', an alias for 'ls' (jnewland) 77 | 78 | 0.1.0 79 | ----- 80 | 81 | - Initial release 82 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011 Mike Perham 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lunchy 2 | 3 | A friendly wrapper for launchctl. Start your agents and go to lunch! 4 | 5 | Don't you hate OSX's launchctl? You have to give it exact filenames, the syntax is annoyingly different from Linux's, and it is overly verbose. It's just not a very developer-friendly tool. 6 | 7 | Lunchy aims to be that friendly tool by wrapping launchctl and providing a few simple operations that you perform all the time: 8 | 9 | - ls [pattern] 10 | - start [pattern] 11 | - stop [pattern] 12 | - restart [pattern] 13 | - status [pattern] 14 | - install [file] 15 | - uninstall [pattern] 16 | - show [pattern] 17 | - edit [pattern] 18 | 19 | where pattern is just a substring that matches the agent's plist filename. If you don't use a unique pattern, Lunchy will warn you of this and give you a list of the matching items instead. 20 | 21 | So instead of: 22 | 23 | launchctl load ~/Library/LaunchAgents/io.redis.redis-server.plist 24 | 25 | you can do this: 26 | 27 | lunchy start redis 28 | 29 | and: 30 | 31 | > lunchy ls 32 | com.danga.memcached 33 | com.google.keystone.agent 34 | com.mysql.mysqld 35 | io.redis.redis-server 36 | org.mongodb.mongod 37 | 38 | The original name was supposed to be launchy. Lunchy isn't a great name but gem names are like domains, most of the good ones are taken. :-( 39 | 40 | 41 | ## Installation 42 | 43 | ### Using RubyGems 44 | 45 | gem install lunchy 46 | 47 | Lunchy is written in Ruby because I'm a good Ruby developer and a poor Bash developer. Help is welcome. 48 | 49 | ### Using Homebrew 50 | 51 | brew install lunchy 52 | 53 | ## Thanks 54 | 55 | Thanks to all the individual contributors who've improved Lunchy, see credits in History.md. 56 | 57 | Lunchy was written as part of my project time at [Carbon Five](http://carbonfive.com). [They're hiring](http://www.carbonfive.com/careers/) if you love working on Ruby and open source. 58 | 59 | 60 | ## About 61 | 62 | * Mike Perham [@mperham](https://github.com/mperham) - [twitter: @mperham](http://twitter.com/mperham), [mikeperham.com](http://mikeperham.com/) 63 | * Eddie Zaneski [@eddiezane](https://github.com/eddiezane) - [twitter: @eddiezane](http://twitter.com/eddiezane), [doesnotscale.com](http://doesnotscale.com) 64 | * Mr Rogers [@bunnymatic](https://github.com/bunnymatic) - [twitter: @rcode5](http://twitter.com/rcode5), [rcode5.com](http://rcode5.com) 65 | * [and more](https://github.com/eddiezane/lunchy/graphs/contributors) 66 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" -------------------------------------------------------------------------------- /bin/lunchy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $LOAD_PATH << File.dirname(__FILE__) + "/../lib" if $0 == __FILE__ 4 | require 'optparse' 5 | require 'lunchy' 6 | 7 | CONFIG = {} 8 | OPERATIONS = %w(start stop restart ls list status install uninstall rm show edit) 9 | 10 | option_parser = OptionParser.new do |opts| 11 | opts.banner = "Lunchy #{Lunchy::VERSION}, the friendly launchctl wrapper\n" \ 12 | "Usage: #{File.basename(__FILE__)} [#{OPERATIONS.join('|')}] [options]" 13 | 14 | opts.on("-x", "--exact", "Force exact (case insensitive) match when specifying a [pattern]") do 15 | CONFIG[:exact] = true 16 | end 17 | 18 | opts.on("-F", "--force", "Force start (disabled) agents") do |verbose| 19 | CONFIG[:force] = true 20 | end 21 | 22 | opts.on("-v", "--verbose", "Show command executions") do |verbose| 23 | CONFIG[:verbose] = true 24 | end 25 | 26 | opts.on("-w", "--write", "Persist command") do |verbose| 27 | CONFIG[:write] = true 28 | end 29 | 30 | opts.on("-l", "--long", "Display absolute paths when listing agents") do 31 | CONFIG[:long] = true 32 | end 33 | 34 | opts.on("-s", "--symlink", "Use a symlink for installation") do |verbose| 35 | CONFIG[:symlink] = true 36 | end 37 | 38 | opts.separator <<-EOS 39 | 40 | Supported commands: 41 | 42 | ls [-l] [pattern] Show the list of installed agents, with optional [pattern] filter 43 | list [-l] [pattern] Alias for 'ls' 44 | start [-wF] [pattern] Start the first agent matching [pattern] 45 | stop [-w] [pattern] Stop the first agent matching [pattern] 46 | restart [pattern] Stop and start the first agent matching [pattern] 47 | status [pattern] Show the PID and label for all agents, with optional [pattern] filter 48 | install [-s] [file] Install [file] to ~/Library/LaunchAgents or /Library/LaunchAgents (whichever it finds first) 49 | uninstall [name] Uninstall [name] from ~/Library/LaunchAgents or /Library/LaunchAgents (whichever it finds first) 50 | show [pattern] Show the contents of the launchctl daemon file 51 | edit [pattern] Open the launchctl daemon file in the default editor (EDITOR environment variable) 52 | 53 | -w will persist the start/stop command so the agent will load on startup or never load, respectively. 54 | -l will display absolute paths of the launchctl daemon files when showing list of installed agents. 55 | -x will force exact matching of the [pattern] for any command that uses a pattern 56 | 57 | Example: 58 | lunchy ls 59 | lunchy ls -l nginx 60 | lunchy start -w redis 61 | lunchy stop mongo 62 | lunchy status mysql 63 | lunchy install /usr/local/Cellar/redis/2.2.2/io.redis.redis-server.plist 64 | lunchy show redis 65 | lunchy edit mongo 66 | lunchy uninstall -x elasticsearch # will not try to uninstall elasticsearch14 67 | 68 | Note: if you run lunchy as root, you can manage daemons in /Library/LaunchDaemons also. 69 | EOS 70 | end 71 | option_parser.parse! 72 | 73 | 74 | op = ARGV.shift 75 | if OPERATIONS.include?(op) 76 | begin 77 | Lunchy.new.send(op.to_sym, ARGV) 78 | rescue ArgumentError => ex 79 | puts ex.message 80 | rescue Exception => e 81 | puts "Uh oh, I didn't expect this:" 82 | puts e.message 83 | puts e.backtrace.join("\n") 84 | end 85 | else 86 | puts option_parser.help 87 | end 88 | -------------------------------------------------------------------------------- /extras/lunchy-completion.bash: -------------------------------------------------------------------------------- 1 | ### 2 | # completion written by Alexey Shockov 3 | # on github : alexeyshockov 4 | # 5 | function _lunchy { 6 | COMPREPLY=() 7 | local cur=${COMP_WORDS[COMP_CWORD]} 8 | local cur_pos=$COMP_CWORD; 9 | 10 | case "$cur_pos" in 11 | 1) COMPREPLY=($(compgen -W 'ls list start stop restart status install uninstall show edit' -- $cur)) 12 | ;; 13 | *) COMPREPLY=($(compgen -W "$(lunchy list)" -- $cur)) 14 | ;; 15 | esac 16 | } 17 | 18 | complete -F _lunchy -o default lunchy 19 | -------------------------------------------------------------------------------- /extras/lunchy-completion.zsh: -------------------------------------------------------------------------------- 1 | ### 2 | # completion written by Raphaël Emourgeon 3 | # on github : osaris 4 | # 5 | 6 | autoload -U bashcompinit 7 | bashcompinit 8 | source $(dirname $0)/lunchy-completion.bash 9 | -------------------------------------------------------------------------------- /lib/lunchy.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | 3 | class Lunchy 4 | VERSION = '0.10.4' 5 | 6 | def start(params) 7 | raise ArgumentError, "start [-wF] [name]" if params.empty? 8 | 9 | with_match params[0] do |name, path| 10 | execute("launchctl load #{force}#{write}#{path.inspect}") 11 | puts "started #{name}" 12 | end 13 | end 14 | 15 | def stop(params) 16 | raise ArgumentError, "stop [-w] [name]" if params.empty? 17 | 18 | with_match params[0] do |name, path| 19 | execute("launchctl unload #{write}#{path.inspect}") 20 | puts "stopped #{name}" 21 | end 22 | end 23 | 24 | def restart(params) 25 | stop(params.dup) 26 | start(params.dup) 27 | end 28 | 29 | def status(params) 30 | pattern = pattern_for_grep params[0] 31 | cmd = "launchctl list" 32 | unless verbose? 33 | agents = plists.keys.map { |k| "-e \"#{k}\"" }.join(" ") 34 | cmd << " | grep -i #{agents}" 35 | end 36 | 37 | cmd.gsub!('.','\.') 38 | cmd << " | grep -i \"#{pattern}\"" if pattern 39 | print "PID\tStatus\tLabel\n" 40 | execute(cmd) 41 | end 42 | 43 | def ls(params) 44 | pattern = pattern_regex params[0] 45 | agents = plists.keys 46 | agents = agents.grep(pattern) if !params.empty? 47 | if long 48 | puts agents.map { |agent| plists[agent] }.sort.join("\n") 49 | else 50 | puts agents.sort.join("\n") 51 | end 52 | end 53 | alias_method :list, :ls 54 | 55 | def install(params) 56 | raise ArgumentError, "install [-s] [file]" if params.empty? 57 | filename = params[0] 58 | %w(~/Library/LaunchAgents /Library/LaunchAgents).each do |dir| 59 | if File.exist?(File.expand_path(dir)) 60 | if symlink 61 | FileUtils.ln_s filename, File.join(File.expand_path(dir), File.basename(filename)), force: true 62 | return puts "#{filename} installed to #{dir}" 63 | else 64 | FileUtils.cp filename, File.join(File.expand_path(dir), File.basename(filename)) 65 | return puts "#{filename} installed to #{dir}" 66 | end 67 | end 68 | end 69 | end 70 | 71 | def uninstall(params) 72 | raise ArgumentError, "uninstall [name]" if params.empty? 73 | 74 | stop(params.dup) 75 | 76 | with_match params[0] do |name, path| 77 | if File.exist?(path) 78 | FileUtils.rm(path) 79 | puts "uninstalled #{name}" 80 | end 81 | end 82 | end 83 | alias_method :rm, :uninstall 84 | 85 | def show(params) 86 | raise ArgumentError, "show [name]" if params.empty? 87 | 88 | with_match params[0] do |_, path| 89 | puts IO.read(path) 90 | end 91 | end 92 | 93 | def edit(params) 94 | raise ArgumentError, "edit [name]" if params.empty? 95 | 96 | with_match params[0] do |_, path| 97 | editor = ENV['EDITOR'] 98 | if editor.nil? 99 | raise 'EDITOR environment variable is not set' 100 | else 101 | execute("#{editor} #{path.inspect} > `tty`") 102 | end 103 | end 104 | end 105 | 106 | private 107 | 108 | def exact 109 | CONFIG[:exact] 110 | end 111 | 112 | def force 113 | CONFIG[:force] and '-F ' 114 | end 115 | 116 | def write 117 | CONFIG[:write] and '-w ' 118 | end 119 | 120 | def long 121 | CONFIG[:long] 122 | end 123 | 124 | def symlink 125 | CONFIG[:symlink] 126 | end 127 | 128 | def pattern_for_grep(s) 129 | exact ? "^#{s}$" : s if s 130 | end 131 | 132 | def pattern_regex(s) 133 | /#{pattern_for_grep(s)}/i 134 | end 135 | 136 | def with_match(name) 137 | files = plists.select {|k,_| k =~ pattern_regex(name) } 138 | files = Hash[files] if files.is_a?(Array) # ruby 1.8 139 | 140 | if files.size > 1 141 | puts "Multiple daemons found matching '#{name}'. You need to be more specific. Matches found are:\n#{files.keys.join("\n")}" 142 | elsif files.empty? 143 | puts "No daemon found matching '#{name}' #{exact ? 'exactly' : nil}" if name 144 | else 145 | yield(*files.to_a.first) 146 | end 147 | end 148 | 149 | def execute(cmd) 150 | puts "Executing: #{cmd}" if verbose? 151 | emitted = `#{cmd}` 152 | puts emitted unless emitted.empty? 153 | end 154 | 155 | def plists 156 | @plists ||= begin 157 | plists = {} 158 | dirs.each do |dir| 159 | Dir["#{File.expand_path(dir)}/*.plist"].inject(plists) do |memo, filename| 160 | memo[File.basename(filename, ".plist")] = filename; memo 161 | end 162 | end 163 | plists 164 | end 165 | end 166 | 167 | def dirs 168 | result = %w(/Library/LaunchAgents ~/Library/LaunchAgents) 169 | result.push('/Library/LaunchDaemons', '/System/Library/LaunchDaemons') if root? 170 | result 171 | end 172 | 173 | def root? 174 | Process.euid == 0 175 | end 176 | 177 | def verbose? 178 | CONFIG[:verbose] 179 | end 180 | end 181 | -------------------------------------------------------------------------------- /lunchy.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require "./lib/lunchy" 3 | 4 | post_install_message = <<-EOS 5 | ------- 6 | 7 | Thanks for installing Lunchy. We know you're going to love it! 8 | 9 | If you want to add tab-completion (for bash), add the following 10 | to your .bash_profile, .bashrc or .profile 11 | 12 | LUNCHY_DIR=$(dirname `gem which lunchy`)/../extras 13 | if [ -f $LUNCHY_DIR/lunchy-completion.bash ]; then 14 | . $LUNCHY_DIR/lunchy-completion.bash 15 | fi 16 | 17 | or add the following to your .zshrc for ZSH 18 | 19 | LUNCHY_DIR=$(dirname `gem which lunchy`)/../extras 20 | if [ -f $LUNCHY_DIR/lunchy-completion.zsh ]; then 21 | . $LUNCHY_DIR/lunchy-completion.zsh 22 | fi 23 | 24 | ------- 25 | EOS 26 | 27 | Gem::Specification.new do |s| 28 | s.name = "lunchy" 29 | s.version = Lunchy::VERSION 30 | s.platform = Gem::Platform::RUBY 31 | s.authors = ["Mike Perham", "Eddie Zaneski", "Mr Rogers"] 32 | s.email = ["mperham@gmail.com"] 33 | s.homepage = "http://github.com/eddiezane/lunchy" 34 | s.summary = s.description = %q{Friendly wrapper around launchctl} 35 | s.post_install_message = post_install_message 36 | s.licenses = ['MIT'] 37 | 38 | s.add_development_dependency "rake" 39 | 40 | s.files = `git ls-files`.split("\n") 41 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 42 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 43 | s.require_paths = ["lib"] 44 | end 45 | --------------------------------------------------------------------------------