crontab.rb 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0b57c9090bd46a927092324804678d34340f5154cb187bac3988ca2e3bd09b20
4
+ data.tar.gz: 1d48233a3ca27b1649ab3ddcabf7e56ca42f081c51e03b2e3816419400547d9b
5
+ SHA512:
6
+ metadata.gz: b64ac038eacb86b1a393d4e3b63b5a315b6411aa083fd19046c5b1b9530f8c3fc3402d28a1548ecb90abfb42c58a45d882cc1e8fb634e95ba9fa388e90dafa8c
7
+ data.tar.gz: 5a1026f121a6dd38e7ef34e304e6686a61caa578b1232a5a575a9148c98a7ef23c8c3796ca532222215813846fc90f70e48b9e080d5c121c5bf47f5e963e6b0c
data/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Nels Nelson
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.
data/README.md ADDED
@@ -0,0 +1,130 @@
1
+ # crontab.rb
2
+
3
+ A user who wishes to define their own cron jobs and file monitors in macOS faces some obstacles.
4
+
5
+ As of 2024, the current state of the art relies on XML plist files to define jobs in the `Library/LaunchAgents` directory in a user's home directory.
6
+
7
+ XML is not a very simple format for a human being to read or write.
8
+
9
+ If you already have the skills to read and write macOS LaunchAgent XML documents with ease, then the authors commend you for your determination and studiousness.
10
+
11
+ Otherwise, for all of you lazy and ignorant people like the authors, this gem is for you!
12
+
13
+ The `crontab-manager` gem provides some little ruby scripts which will support simpler management of such launch agents.
14
+
15
+
16
+ ## Usage
17
+
18
+ Here is some usage and examples for each script.
19
+
20
+
21
+ ### cron.rb
22
+
23
+ The usage for `cron.rb`.
24
+
25
+ The `cron.rb` executable uses something similar to a `crontab` entry in GNU/Linux.
26
+
27
+ ```sh
28
+ $ cron.rb --help
29
+ Usage: cron.rb <crontab>|<options>
30
+
31
+ Options:
32
+ --show-all Show all cron jobs
33
+ -l, --list List launch agent labels
34
+ -r, --remove=<label> Remove a cron job by label
35
+ ```
36
+
37
+
38
+ #### Example
39
+
40
+ For example:
41
+
42
+ ```sh
43
+ cron.rb "0 1 * * * archive.rb ~/Downloads/archive"
44
+ ```
45
+
46
+
47
+ ### watch.rb
48
+
49
+ The usage for `watch.rb`.
50
+
51
+ ```sh
52
+ $ watch.rb --help
53
+ Usage: watch.rb <watch_path> <handler_program_path>
54
+
55
+ Options:
56
+ --show-all Show watchers
57
+ -l, --list List launch agent labels
58
+ -r, --remove=<label> Remove a watcher by label
59
+ ```
60
+
61
+
62
+ #### Example
63
+
64
+ For example:
65
+
66
+ ```sh
67
+ watch.rb ~/Desktop ~/.local/usr/bin/desktop.rb
68
+ ```
69
+
70
+
71
+ ## Docker
72
+
73
+ Build the docker image to support CI/CD containers.
74
+
75
+ ```sh
76
+ docker buildx build --tag="$(basename $(pwd))" .
77
+ docker run --interactive --tty --rm --name "$(basename $(pwd))" "$(basename $(pwd))"
78
+ ```
79
+
80
+
81
+ ## Project file tree
82
+
83
+ Here is a bird's-eye view of the project layout.
84
+
85
+ ```sh
86
+ date && tree -A -I "Gemfile.lock|*.gem|tmp|vendor"
87
+ Sun Jan 11 00:55:39 CST 2026
88
+ .
89
+ ├── bin
90
+ │ ├── cron.rb
91
+ │ ├── login.rb
92
+ │ └── watch.rb
93
+ ├── crontab.rb.gemspec
94
+ ├── crontab.rb.png
95
+ ├── Dockerfile
96
+ ├── Gemfile
97
+ ├── lib
98
+ │ ├── crontab.rb
99
+ │ └── crontab.rb.d
100
+ │ ├── cron
101
+ │ │ ├── argument_parser.rb
102
+ │ │ └── launch_agent_manager_rexml.rb
103
+ │ ├── cron.rb
104
+ │ ├── launch_agent_constants.rb
105
+ │ ├── launch_agent_manager.rb
106
+ │ ├── logging.rb
107
+ │ ├── login
108
+ │ │ └── argument_parser.rb
109
+ │ ├── plist_parser.rb
110
+ │ ├── version.rb
111
+ │ ├── watch
112
+ │ │ └── argument_parser.rb
113
+ │ ├── watch.rb
114
+ │ ├── which.rb
115
+ │ ├── xml_helper_rexml.rb
116
+ │ └── xml_helper.rb
117
+ ├── LICENSE.md
118
+ ├── Rakefile
119
+ ├── README.md
120
+ ├── scripts
121
+ │ └── version_bump.sh
122
+ └── spec
123
+ ├── spec_helper.rb
124
+ ├── test_spec.rb
125
+ └── verify
126
+ ├── launchctl.rb
127
+ └── verify_spec.rb
128
+
129
+ 9 directories, 30 files
130
+ ```
data/bin/cron.rb ADDED
@@ -0,0 +1,27 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ # encoding: utf-8
4
+ # frozen_string_literal: false
5
+
6
+ # -*- mode: ruby -*-
7
+ # vi: set ft=ruby :
8
+
9
+ # Copyright Nels Nelson 2024 but freely usable (see license)
10
+
11
+ # Usage: cron.rb <crontab>|<options>
12
+ #
13
+ # Options:
14
+ # --show-all Show cron jobs
15
+ # -l, --list List cron jobs labels
16
+ # -r, --remove=<label> Remove a cron job by label
17
+ #
18
+ # Examples:
19
+ #
20
+ # cron.rb "0 1 * * * archive.rb ${HOME}/Downloads/archive"
21
+ # cron.rb --show-all
22
+ # cron.rb --list
23
+ # cron.rb --remove=com.local.archive
24
+
25
+ require_relative '../lib/crontab'
26
+
27
+ Object.new.extend(Cron).main
data/bin/login.rb ADDED
@@ -0,0 +1,27 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ # encoding: utf-8
4
+ # frozen_string_literal: false
5
+
6
+ # -*- mode: ruby -*-
7
+ # vi: set ft=ruby :
8
+
9
+ # Copyright Nels Nelson 2024 but freely usable (see license)
10
+
11
+ # Usage: watch.rb <path> [handler_program_path]
12
+ #
13
+ # Options:
14
+ # --show-all Show watchers
15
+ # -l, --list List launch agent labels
16
+ # -r, --remove=<label> Remove a watcher by label
17
+ #
18
+ # Examples:
19
+ #
20
+ # watch.rb ~/Desktop ~/.local/usr/bin/desktop.rb
21
+ # watch.rb --show-all
22
+ # watch.rb --list
23
+ # watch.rb --remove=com.local.desktop
24
+
25
+ require_relative '../lib/crontab'
26
+
27
+ Object.new.extend(Login).main
data/bin/watch.rb ADDED
@@ -0,0 +1,27 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ # encoding: utf-8
4
+ # frozen_string_literal: false
5
+
6
+ # -*- mode: ruby -*-
7
+ # vi: set ft=ruby :
8
+
9
+ # Copyright Nels Nelson 2024 but freely usable (see license)
10
+
11
+ # Usage: watch.rb <path> [handler_program_path]
12
+ #
13
+ # Options:
14
+ # --show-all Show watchers
15
+ # -l, --list List launch agent labels
16
+ # -r, --remove=<label> Remove a watcher by label
17
+ #
18
+ # Examples:
19
+ #
20
+ # watch.rb ~/Desktop ~/.local/usr/bin/desktop.rb
21
+ # watch.rb --show-all
22
+ # watch.rb --list
23
+ # watch.rb --remove=com.local.desktop
24
+
25
+ require_relative '../lib/crontab'
26
+
27
+ Object.new.extend(Watcher).main
data/lib/crontab.rb ADDED
@@ -0,0 +1,10 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: false
3
+
4
+ # vi: set ft=ruby :
5
+ # -*- mode: ruby -*-
6
+
7
+ # Copyright Nels Nelson 2024 but freely usable (see license)
8
+
9
+ require_relative 'crontab.rb.d/cron'
10
+ require_relative 'crontab.rb.d/watch'
@@ -0,0 +1,120 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: false
3
+
4
+ # vi: set ft=ruby :
5
+ # -*- mode: ruby -*-
6
+
7
+ # Copyright Nels Nelson 2024 but freely usable (see license)
8
+
9
+ require 'logger'
10
+ require 'optparse'
11
+
12
+ require_relative '../version'
13
+
14
+ # Define module Cron
15
+ module Cron
16
+ # Define the ArgumentsParser class
17
+ class ArgumentsParser
18
+ FLAGS = %i[banner show_all list remove verbose version].freeze
19
+ POSITIONAL = %i[crontab].freeze
20
+ attr_reader :parser, :options
21
+
22
+ def initialize(option_parser = OptionParser.new)
23
+ @parser = option_parser
24
+ @options = {}
25
+ FLAGS.each { |method_name| self.method(method_name).call }
26
+ end
27
+
28
+ def banner
29
+ @parser.banner = "Usage: #{File.basename($PROGRAM_NAME)} <crontab>|<options>"
30
+ @parser.separator ''
31
+ @parser.separator 'Options:'
32
+ end
33
+
34
+ def show_all
35
+ @parser.on('--show-all', 'Show cron jobs') do
36
+ @options[:show_all] = true
37
+ end
38
+ end
39
+
40
+ def list
41
+ @parser.on('-l', '--list', 'List cron jobs labels') do
42
+ @options[:list] = true
43
+ end
44
+ end
45
+
46
+ def remove
47
+ @parser.on('-r', '--remove=<label>', 'Remove a cron job by label') do |label|
48
+ @options[:remove] = label
49
+ end
50
+ end
51
+
52
+ def verbose
53
+ @options[:log_level] ||= Logger::INFO
54
+ @parser.on_tail('-v', '--verbose', 'Increase verbosity') do
55
+ @options[:log_level] -= 1
56
+ end
57
+ end
58
+
59
+ def version
60
+ @parser.on_tail('--version', 'Show version') do
61
+ puts "#{File.basename($PROGRAM_NAME)} version #{CrontabRb::VERSION}"
62
+ exit
63
+ end
64
+ end
65
+
66
+ def crontab(args)
67
+ crontab = args.shift.gsub(/\A['"]|['"]\z/, '').split
68
+ @options[:crontab] = crontab
69
+ @options[:cron_schedule] =
70
+ crontab.take(LaunchAgentManager::SCHEDULE_PARTS_COUNT).join(' ')
71
+ @options[:executable_path_with_args] =
72
+ crontab.drop(LaunchAgentManager::SCHEDULE_PARTS_COUNT).join(' ')
73
+ end
74
+
75
+ def demand(arg, positional: false)
76
+ return @options[arg] unless @options[arg].nil?
77
+
78
+ required_arg = if positional then "<#{arg}>"
79
+ else "--#{arg.to_s.gsub(UNDERSCORE_PATTERN, HYPHEN_STRING)}"
80
+ end
81
+ raise OptionParser::MissingArgument, "Required argument: #{required_arg}"
82
+ end
83
+
84
+ def positional!(args)
85
+ POSITIONAL.each do |opt|
86
+ self.method(opt).call(args)
87
+ self.demand(opt, positional: true)
88
+ end
89
+ end
90
+
91
+ def options?
92
+ ArgumentsParser::FLAGS.any? { |flag| @options.include?(flag) }
93
+ end
94
+
95
+ def usage!
96
+ puts @parser
97
+ exit
98
+ end
99
+
100
+ # rubocop: disable Metrics/MethodLength
101
+ def self.parse(args = ARGV, _file_path = ARGF, arguments_parser = ArgumentsParser.new)
102
+ arguments_parser.parser.parse!(args)
103
+ if !arguments_parser.options? && ARGV.length != 1
104
+ message = 'A crontab definition is required'
105
+ raise OptionParser::MissingArgument, message
106
+ elsif !args.empty?
107
+ arguments_parser.positional!(args)
108
+ end
109
+ arguments_parser.options
110
+ rescue OptionParser::AmbiguousOption => e
111
+ abort e.message
112
+ rescue OptionParser::ParseError => e
113
+ puts e.message
114
+ arguments_parser.usage!
115
+ end
116
+ # rubocop: enable Metrics/MethodLength
117
+ end
118
+ # class ArgumentsParser
119
+ end
120
+ # module Cron
@@ -0,0 +1,183 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: false
3
+
4
+ # vi: set ft=ruby :
5
+ # -*- mode: ruby -*-
6
+
7
+ # Copyright Nels Nelson 2024 but freely usable (see license)
8
+
9
+ require 'rexml/formatters/pretty'
10
+
11
+ require 'fileutils'
12
+
13
+ require_relative '../launch_agent_plist'
14
+
15
+ # Define module LaunchAgentManagerConstants
16
+ module LaunchAgentManagerConstants
17
+ VERSION = '0.1.0'.freeze unless defined?(VERSION)
18
+ LABEL_NAMESPACE = %w[com local].freeze
19
+ INTERVALS = %w[Minute Hour Day Month Weekday].freeze
20
+ SCHEDULE_PARTS_COUNT = INTERVALS.length
21
+ LAUNCHCTL_TEMPLATE = 'launchctl bootstrap gui/%<uid>s %<plist>s'.freeze
22
+ SUCCESS_MESSAGE = 'Created and enabled launchd job: %<label>s'.freeze
23
+ LAUNCH_AGENTS_DIR_PATH = File.expand_path(File.join(Dir.home, 'Library', 'LaunchAgents'))
24
+ BOOTOUT_TEMPLATE = 'launchctl bootout gui/%<uid>s/%<label>s'.freeze
25
+ REMOVE_TEMPLATE = 'launchctl remove gui/%<uid>s/%<label>s'.freeze
26
+ end
27
+
28
+ # Define module LaunchAgentManagerInstanceMethods
29
+ module LaunchAgentManagerInstanceMethods
30
+ include LaunchAgentManagerConstants
31
+
32
+ def find_executable_in_path(cmd)
33
+ directory = ENV['PATH'].split(File::PATH_SEPARATOR).find do |dir|
34
+ File.executable?(File.join(dir, cmd))
35
+ end
36
+ directory.nil? ? nil : File.join(directory, cmd)
37
+ end
38
+
39
+ def executable?(exe)
40
+ !exe.empty? && File.executable?(exe) && !File.directory?(exe)
41
+ end
42
+
43
+ def explicit_which(cmd)
44
+ exe = `which #{cmd}`.chomp
45
+ executable?(exe) ? exe : nil
46
+ rescue Errno::ENOENT => _e
47
+ nil
48
+ end
49
+
50
+ def portable_which(cmd)
51
+ explicit_which(cmd) || find_executable_in_path(cmd)
52
+ end
53
+
54
+ # Extract program path and arguments, verifying executable
55
+ # rubocop: disable Metrics/MethodLength
56
+ def extract_program_path(executable_with_args)
57
+ *args = executable_with_args.split
58
+ exe = args.shift
59
+ exe_path = File.exist?(exe) ? exe : portable_which(exe)
60
+ if exe_path.nil? || !File.exist?(exe_path)
61
+ abort "Cannot find executable in path: #{exe}"
62
+ end
63
+ if exe_path.nil? || !File.executable?(exe_path) || File.directory?(exe_path)
64
+ abort "Given file is not executable: #{exe}"
65
+ end
66
+ args.unshift(exe_path)
67
+ args
68
+ end
69
+ # rubocop: enable Metrics/MethodLength
70
+
71
+ # Create the job name based on the given executable file path
72
+ def derive_job_label_from_executable(exe_path)
73
+ label = LABEL_NAMESPACE.dup
74
+ label << File.basename(exe_path, '.*')
75
+ label.join('.')
76
+ end
77
+
78
+ # Define cron settings for plist
79
+ def derive_calendar_interval(cron_schedule)
80
+ cron_fields = cron_schedule.split
81
+ calendar_interval = {}
82
+ INTERVALS.each do |key|
83
+ value = cron_fields.shift
84
+ raise "Invalid cron string: #{cron_schedule}" if value.nil? || value.empty?
85
+
86
+ calendar_interval[key] = value
87
+ end
88
+ calendar_interval
89
+ end
90
+
91
+ # Save plist file to LaunchAgents directory; do not overwrite an
92
+ # already existing file with the same name at the generated path
93
+ # rubocop: disable Metrics/MethodLength
94
+ def save_plist(label, doc)
95
+ FileUtils.mkdir_p(LAUNCH_AGENTS_DIR_PATH)
96
+ plist_path = File.join(LAUNCH_AGENTS_DIR_PATH, "#{label}.plist")
97
+ puts "plist_path: #{plist_path}"
98
+ return plist_path if File.exist?(plist_path)
99
+
100
+ document = ''
101
+ formatter = REXML::Formatters::Pretty.new(4)
102
+ formatter.write(doc, document)
103
+ puts '[DEBUG] Contents of plist xml document:'
104
+ puts document
105
+ File.write(plist_path, document)
106
+ plist_path
107
+ end
108
+ # rubocop: enable Metrics/MethodLength
109
+
110
+ def execute(command)
111
+ puts "[DEBUG] Executing command: #{command}"
112
+ system(command)
113
+ end
114
+
115
+ # Load the launchd job
116
+ def load_launchd_job(plist_path)
117
+ execute(format(LAUNCHCTL_TEMPLATE, uid: Process.uid, plist: plist_path))
118
+ end
119
+
120
+ # Show the launchd agents as crontabs
121
+ def show_all_cron_jobs
122
+ Dir.glob(File.join(LAUNCH_AGENTS_DIR_PATH, '*.plist')).map do |plist_path|
123
+ job = parse_plist(plist_path)
124
+ puts "#{job[:schedule]} #{job[:command]}"
125
+ end
126
+ end
127
+
128
+ # Return a list of all user launchd agent labels
129
+ def launch_agent_labels
130
+ plist_file_paths = Dir.glob(File.join(LAUNCH_AGENTS_DIR_PATH, '*.plist'))
131
+ plist_file_paths.each_with_object([]) do |file_path, labels|
132
+ label = LaunchAgentPlist.parse(file_path)[:label]
133
+ labels << label unless label.nil?
134
+ end
135
+ end
136
+
137
+ # List labels of launchd agents
138
+ def list_launch_agent_labels
139
+ launch_agent_labels.each do |label|
140
+ puts label
141
+ end
142
+ end
143
+
144
+ # Remove plist Launch Agents by label
145
+ def remove_cron_job(label)
146
+ # warn "[DEBUG] Removing launch agent: #{label}"
147
+ plist_path = File.expand_path(File.join(LAUNCH_AGENTS_DIR_PATH, "#{label}.plist"))
148
+ # warn "[DEBUG] Removing launch agent plist definition file: #{plist_path}"
149
+ if File.exist?(plist_path)
150
+ execute(format(BOOTOUT_TEMPLATE, uid: Process.uid, label: label))
151
+ execute(format(REMOVE_TEMPLATE, uid: Process.uid, label: label))
152
+ FileUtils.rm(plist_path)
153
+ puts "Removed launch agent: #{label}"
154
+ else
155
+ warn "Not found; crontab or launch agent: #{label}"
156
+ end
157
+ end
158
+
159
+ # Create the launchd job
160
+ def create_launchd_job(options)
161
+ args = extract_program_path(options[:executable_path_with_args])
162
+ label = derive_job_label_from_executable(args.first)
163
+ calendar_interval = derive_calendar_interval(options[:cron_schedule])
164
+ plist_doc = LaunchAgentPlist.new(label, args, calendar_interval).to_doc
165
+ plist_path = save_plist(label, plist_doc)
166
+ puts "Executing: cat #{plist_path}"
167
+ puts `cat #{plist_path}`.strip
168
+ load_launchd_job(plist_path)
169
+ puts format(SUCCESS_MESSAGE, label: label)
170
+ end
171
+ end
172
+ # module LaunchAgentManagerInstanceMethods
173
+
174
+ # Define the LaunchAgentManager class
175
+ class LaunchAgentManager
176
+ include LaunchAgentManagerInstanceMethods
177
+
178
+ attr_reader :options
179
+
180
+ def initialize(options)
181
+ @options = options
182
+ end
183
+ end
@@ -0,0 +1,52 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: false
3
+
4
+ # vi: set ft=ruby :
5
+ # -*- mode: ruby -*-
6
+
7
+ # Copyright Nels Nelson 2024 but freely usable (see license)
8
+
9
+ # Usage: cron.rb <crontab>|<options>
10
+ #
11
+ # Options:
12
+ # --show-all Show cron jobs
13
+ # -l, --list List cron jobs labels
14
+ # -r, --remove=<label> Remove a cron job by label
15
+ #
16
+ # Examples:
17
+ #
18
+ # cron.rb "0 1 * * * archive.rb ${HOME}/Downloads/archive"
19
+ # cron.rb --show-all
20
+ # cron.rb --list
21
+ # cron.rb --remove=com.local.archive
22
+
23
+ require_relative 'launch_agent_manager'
24
+ require_relative 'cron/argument_parser'
25
+
26
+ # Define module Cron
27
+ module Cron
28
+ LaunchAgentManager = ::LaunchAgentManager
29
+
30
+ # rubocop: disable Metrics/MethodLength
31
+ def main(args = Cron::ArgumentsParser.parse)
32
+ log.log_level = args[:log_level]
33
+ manager = Cron::LaunchAgentManager.new(args)
34
+ if args[:show_all]
35
+ manager.show_all_launch_agents
36
+ elsif args[:list]
37
+ manager.list_launch_agent_labels
38
+ elsif args[:remove]
39
+ manager.remove_launch_agent(args[:remove])
40
+ else
41
+ manager.create_launch_agent(
42
+ args[:executable_path_with_args],
43
+ nil,
44
+ args[:cron_schedule]
45
+ )
46
+ end
47
+ end
48
+ # rubocop: enable Metrics/MethodLength
49
+ end
50
+ # module Cron
51
+
52
+ Object.new.extend(Cron).main if $PROGRAM_NAME == __FILE__
@@ -0,0 +1,41 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: false
3
+
4
+ # -*- mode: ruby -*-
5
+ # vi: set ft=ruby :
6
+
7
+ # Copyright Nels Nelson 2024 but freely usable (see license)
8
+
9
+ # Define the LaunchAgentConstants module
10
+ module LaunchAgentConstants
11
+ DEFAULT_ENCODING = 'UTF-8'.freeze
12
+ DOCTYPE_ELEMENT = '<!%<doctypes>s>'.freeze
13
+ DOCTYPES = [
14
+ 'DOCTYPE',
15
+ 'plist',
16
+ 'PUBLIC',
17
+ '"-//Apple//DTD PLIST 1.0//EN"',
18
+ '"http://www.apple.com/DTDs/PropertyList-1.0.dtd"'
19
+ ].freeze
20
+ LABEL_NAMESPACE = %w[com local].freeze
21
+ INTERVALS = %w[Minute Hour Day Month Weekday].freeze
22
+ SCHEDULE_PARTS_COUNT = INTERVALS.length
23
+ CDATA_PATTERN = %r{[<>&'"\n]+}
24
+ SCHEDULE_XPATH_TEMPLATE = './/key[text()="%<key>s"]/following-sibling::integer'.freeze
25
+ KEYS_REQUIRING_CDATA = ['PATH'].freeze
26
+ LAUNCHCTL_TEMPLATE = 'launchctl bootstrap gui/%<uid>s %<plist>s'.freeze
27
+ SUCCESS_MESSAGE = 'Created and enabled launchd job: %<label>s'.freeze
28
+ LAUNCH_AGENTS_DIR_PATH = File.expand_path(File.join(Dir.home, 'Library', 'LaunchAgents'))
29
+ LABEL_XPATH = '//key[text()="Label"]/following-sibling::*[1]'.freeze
30
+ START_CALENDAR_INTERVAL_XPATH =
31
+ '//key[text()="StartCalendarInterval"]/following-sibling::*[1]'.freeze
32
+ WATCH_PATHS_XPATH = '//key[text()="WatchPaths"]/following-sibling::*[1]'.freeze
33
+ LOCAL_STRING_XPATH = './string'.freeze
34
+ PROGRAM_ARGUMENTS_XPATH =
35
+ '//key[text()="ProgramArguments"]/following-sibling::array[1]'.freeze
36
+ INTERVAL_XPATH_TEMPLATE = './key[text()="%<key>s"]/following-sibling::*[1]'.freeze
37
+ BOOTOUT_TEMPLATE = 'launchctl bootout gui/%<uid>s/%<label>s'.freeze
38
+ REMOVE_TEMPLATE = 'launchctl remove gui/%<uid>s/%<label>s'.freeze
39
+ ON_LOGIN = '@login'.freeze
40
+ end
41
+ # module LaunchAgentConstants