sifttter-redux 0.5.4 → 0.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ac0c51854f8d85fe836c9bec7be780a4d149ef55
4
- data.tar.gz: 83c7b28fe6fae8fae9c55d1e95c9b845a1fa5817
3
+ metadata.gz: 6967ac35281de5c0dd08146937c43fee382de50d
4
+ data.tar.gz: 67c9c4c56ceeea7e137cf351642b7beaf4cc6862
5
5
  SHA512:
6
- metadata.gz: 952c9d9cc4d4e68ced1d567278d4294fb5551ad7c5f9ff11f9fc4ba1a8ec847137dad74f29124ae45d3d3bf6db51cf13e6a6ceb6857f96d49a3e02b6b2895b3f
7
- data.tar.gz: 3f8c244c3a6872727f775a2973ecbadfdbb1ad46730cae3f05f742bab981b0f6835cd8af6dc641f86fee6447f3c599852700fae9b58f4f827491f3d6ef2c84ef
6
+ metadata.gz: 5f270d0de834059c6eb902f8952266614ae55f081ce4c415f50b3ccae13edeb9013c4fda4c36e86ddf4d9945132d7e5c98fa42a723faa4a07207b7f39bf564c3
7
+ data.tar.gz: 1b3494e3085c982ea906e36480ad7badf9f9f6ff982d07c7bf630151948b1c16baaa29a88a621b5373e5d6fb730f07fb43ce79dfe5b980559c6aa47596a57e4c
data/.gitignore CHANGED
@@ -3,3 +3,4 @@ Gemfile.lock
3
3
  .DS_Store
4
4
  results.html
5
5
  *.gem
6
+ features/3.execution.feature
data/.travis.yml CHANGED
@@ -3,4 +3,8 @@ rvm:
3
3
  - 2.1.0
4
4
  - 2.0.0
5
5
  - 1.9.3
6
- - jruby-19mode
6
+ - jruby-19mode
7
+ env:
8
+ - TEST_SUITE=1.ui.feature
9
+ script:
10
+ - bundle exec cucumber -f progress -r features features/$TEST_SUITE
data/Gemfile CHANGED
@@ -1,2 +1,3 @@
1
1
  source 'https://rubygems.org'
2
+
2
3
  gemspec
data/Rakefile CHANGED
@@ -1,22 +1,25 @@
1
1
  require 'rake/clean'
2
2
  require 'rubygems'
3
- require 'rubygems/package_task'
4
- require 'rdoc/task'
5
- require 'cucumber'
6
- require 'cucumber/rake/task'
7
- Rake::RDocTask.new do |rd|
8
- rd.main = "README.rdoc"
9
- rd.rdoc_files.include("README.rdoc","lib/**/*.rb","bin/**/*")
10
- rd.title = 'Your application title'
3
+
4
+ def version
5
+ contents = File.read File.expand_path('../lib/sifttter-redux/constants.rb', __FILE__)
6
+ contents[/VERSION = '([^']+)'/, 1]
11
7
  end
12
8
 
13
- spec = eval(File.read('sifttter_redux.gemspec'))
9
+ spec = eval(File.read('sifttter-redux.gemspec'))
14
10
 
15
- Gem::PackageTask.new(spec) do |pkg|
11
+ require 'rake/testtask'
12
+ desc 'Run unit tests'
13
+ Rake::TestTask.new do |t|
14
+ t.libs << "test"
15
+ t.test_files = FileList['test/*_test.rb']
16
16
  end
17
+
18
+ require 'cucumber'
19
+ require 'cucumber/rake/task'
17
20
  CUKE_RESULTS = 'results.html'
18
21
  CLEAN << CUKE_RESULTS
19
- desc 'Run features'
22
+ desc 'Run Cucumber features'
20
23
  Cucumber::Rake::Task.new(:features) do |t|
21
24
  opts = "features --format html -o #{CUKE_RESULTS} --format progress -x"
22
25
  opts += " --tags #{ENV['TAGS']}" if ENV['TAGS']
@@ -24,21 +27,71 @@ Cucumber::Rake::Task.new(:features) do |t|
24
27
  t.fork = false
25
28
  end
26
29
 
27
- desc 'Run features tagged as work-in-progress (@wip)'
28
- Cucumber::Rake::Task.new('features:wip') do |t|
29
- tag_opts = ' --tags ~@pending'
30
- tag_opts = ' --tags @wip'
31
- t.cucumber_opts = "features --format html -o #{CUKE_RESULTS} --format pretty -x -s#{tag_opts}"
32
- t.fork = false
30
+ desc "Release Sifttter Redux version #{ version }"
31
+ task :release => :build do
32
+ unless `git branch` =~ /^\* master$/
33
+ puts "You must be on the master branch to release!"
34
+ exit!
35
+ end
36
+
37
+ sh "git commit --allow-empty -a -m 'Release #{ version }'"
38
+ sh "git tag v#{ version }"
39
+ sh "git push origin master"
40
+ sh "git push origin v#{ version }"
41
+ sh "gem push pkg/sifttter-redux-#{ version }.gem"
33
42
  end
34
43
 
35
- task :cucumber => :features
36
- task 'cucumber:wip' => 'features:wip'
37
- task :wip => 'features:wip'
38
- require 'rake/testtask'
39
- Rake::TestTask.new do |t|
40
- t.libs << "test"
41
- t.test_files = FileList['test/*_test.rb']
44
+ desc "Build the gem"
45
+ task :build do
46
+ p version
47
+ FileUtils.mkdir_p "pkg"
48
+ sh "gem build sifttter-redux.gemspec"
49
+ FileUtils.mv("./sifttter-redux-#{ version }.gem", "pkg")
42
50
  end
43
51
 
44
- task :default => [:test]
52
+ task :default => [:test, :features]
53
+
54
+ # require 'rake/clean'
55
+ # require 'rubygems'
56
+ # require 'rubygems/package_task'
57
+ # require 'rdoc/task'
58
+ # require 'cucumber'
59
+ # require 'cucumber/rake/task'
60
+ # Rake::RDocTask.new do |rd|
61
+ # rd.main = "README.rdoc"
62
+ # rd.rdoc_files.include("README.rdoc","lib/**/*.rb","bin/**/*")
63
+ # rd.title = 'Your application title'
64
+ # end
65
+ #
66
+ # spec = eval(File.read('sifttter_redux.gemspec'))
67
+ #
68
+ # Gem::PackageTask.new(spec) do |pkg|
69
+ # end
70
+ # CUKE_RESULTS = 'results.html'
71
+ # CLEAN << CUKE_RESULTS
72
+ # desc 'Run features'
73
+ # Cucumber::Rake::Task.new(:features) do |t|
74
+ # opts = "features --format html -o #{CUKE_RESULTS} --format progress -x"
75
+ # opts += " --tags #{ENV['TAGS']}" if ENV['TAGS']
76
+ # t.cucumber_opts = opts
77
+ # t.fork = false
78
+ # end
79
+ #
80
+ # desc 'Run features tagged as work-in-progress (@wip)'
81
+ # Cucumber::Rake::Task.new('features:wip') do |t|
82
+ # tag_opts = ' --tags ~@pending'
83
+ # tag_opts = ' --tags @wip'
84
+ # t.cucumber_opts = "features --format html -o #{CUKE_RESULTS} --format pretty -x -s#{tag_opts}"
85
+ # t.fork = false
86
+ # end
87
+ #
88
+ # task :cucumber => :features
89
+ # task 'cucumber:wip' => 'features:wip'
90
+ # task :wip => 'features:wip'
91
+ # require 'rake/testtask'
92
+ # Rake::TestTask.new do |t|
93
+ # t.libs << "test"
94
+ # t.test_files = FileList['test/*_test.rb']
95
+ # end
96
+ #
97
+ # task :default => [:test, :features]
data/bin/srd CHANGED
@@ -32,12 +32,14 @@
32
32
  # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
33
33
  # OTHER DEALINGS IN THE SOFTWARE.
34
34
  #--------------------------------------------------------------------
35
+ require 'cliutils'
35
36
  require 'gli'
36
- require 'sifttter_redux'
37
+ require 'sifttter-redux'
37
38
  require 'securerandom'
38
39
 
40
+ include CLIUtils::Configuration
41
+ include CLIUtils::Messenging
39
42
  include GLI::App
40
- include SifttterRedux
41
43
 
42
44
  # ======================================================
43
45
  # App Info
@@ -48,7 +50,7 @@ program_desc 'Sifttter Redux
48
50
  smart installation and automated running on a standalone
49
51
  *NIX device (such as a Raspberry Pi).'
50
52
 
51
- version VERSION
53
+ version SifttterRedux::VERSION
52
54
 
53
55
  # ======================================================
54
56
  # Global Flags and Switches
@@ -61,44 +63,55 @@ switch([:verbose], desc: 'Turns on verbose output')
61
63
  pre do |global, command, options, args|
62
64
 
63
65
  # Load SifttterRedux configuration module.
64
- Configuration::load(SRD_CONFIG_FILEPATH)
66
+ load_configuration(SifttterRedux::DEFAULT_SRD_CONFIG_FILEPATH)
67
+ file_logger = Logger.new(SifttterRedux::DEFAULT_SRD_LOG_FILEPATH)
68
+ file_logger.level = LOG_LEVELS[configuration.sifttter_redux[:log_level] || 'DEBUG']
69
+ messenger.attach(LOGFILE: file_logger)
65
70
 
66
- # Set up logging.
67
- CLIMessage::activate_logging
68
- CLIMessage::log_level(Configuration::sifttter_redux[:log_level] || Logger::WARN)
71
+ if File.exists?(SifttterRedux::DEFAULT_SRD_CONFIG_FILEPATH)
72
+ # Check to see if there is a new config file version
73
+ # to be installed.
74
+ c_version = Gem::Version.new(configuration.sifttter_redux[:version])
75
+ l_version = Gem::Version.new(SifttterRedux::NEWEST_CONFIG_VERSION)
69
76
 
70
- if File.exists?(SRD_CONFIG_FILEPATH)
71
- current_version = Gem::Version.new(Configuration::sifttter_redux[:version])
72
- last_config_change_version = Gem::Version.new(NEWEST_CONFIG_VERSION)
77
+ messenger.debug { "Current gem version: #{ c_version }" }
78
+ messenger.debug { "Last version with config change: #{ l_version }" }
73
79
 
74
80
  # If the config file needs updating, force the user to do that first.
75
- if Configuration::sifttter_redux[:version].nil? || current_version < last_config_change_version
76
- CLIMessage::info('This version needs to make some config changes.')
77
- CLIMessage::info("Don't worry; when prompted, your current values for")
78
- CLIMessage::info("existing config options will be presented (so it'll")
79
- CLIMessage::info('be easier to fly through the upgrade).')
80
- CLIMessage::prompt('Press enter to continue')
81
- SifttterRedux::init(true)
81
+ if configuration.sifttter_redux[:version].nil? || c_version < l_version
82
+ SifttterRedux.update_config_file
82
83
  exit!(0)
83
84
  end
84
85
  else
85
86
  # Force the user to init if they try to run any command other than `init` first.
86
- SifttterRedux::init(true)
87
+ SifttterRedux.init(true)
87
88
  exit!(0)
88
89
  end
89
90
 
90
91
  true
91
92
  end
92
93
 
94
+ post do |global,command,options,args|
95
+ # Post logic here
96
+ # Use skips_post before a command to skip this
97
+ # block on that command only
98
+ end
99
+
100
+ on_error do |exception|
101
+ messenger.error(exception.to_s)
102
+ exit!(1)
103
+ true
104
+ end
105
+
93
106
  # ======================================================
94
107
  # Commands
95
108
  # ======================================================
96
109
  # ------------------------------------------------------
97
110
  # exec command
98
111
  #
99
- # Executes the script.
112
+ # Executes the app.
100
113
  # ------------------------------------------------------
101
- desc 'Execute the script'
114
+ desc 'Execute the app'
102
115
  command :exec do |c|
103
116
  c.flag([:f], desc: 'Run catch-up mode with this start date')
104
117
  c.flag([:n], desc: 'Run catch-up mode for the last N days')
@@ -113,49 +126,44 @@ command :exec do |c|
113
126
  c.action do |global_options, options, args|
114
127
  SifttterRedux.verbose = global_options[:verbose] || options[:verbose]
115
128
 
116
- begin
117
- dates = SifttterRedux::get_dates_from_options(options)
118
- unless dates.nil?
119
- first_date = dates.first
120
- second_date = dates.reverse_each.first
121
-
122
- date_string = first_date.strftime('%B %d, %Y')
123
- date_string << " to #{ second_date.strftime('%B %d, %Y') }" if first_date != second_date
124
- CLIMessage::info("Creating #{ first_date == second_date ? 'entry' : 'entries' }: #{ date_string }")
125
-
126
- # Download Sifttter files from Dropbox.
127
- dbu = DropboxUploader.new(Configuration::db_uploader[:exe_filepath])
128
- dbu.verbose = SifttterRedux.verbose
129
- dbu.local_target = Configuration::sifttter_redux[:sifttter_local_filepath]
130
- dbu.remote_target = Configuration::sifttter_redux[:sifttter_remote_filepath]
131
- dbu.message = 'Downloading Sifttter files...'
132
-
133
- CLIMessage::info_block(dbu.message || dbu::DEFAULT_MESSAGE, 'Done.', SifttterRedux.verbose) do
134
- dbu.download
135
- end
129
+ dates = SifttterRedux::get_dates_from_options(options)
130
+ unless dates.nil?
131
+ first_date = dates.first
132
+ second_date = dates.reverse_each.first
136
133
 
137
- # Process a new Sifttter entry for each date.
138
- dates.each do |date|
139
- Sifttter::run(date)
140
- end
134
+ date_string = first_date.strftime('%B %d, %Y')
135
+ date_string << " to #{ second_date.strftime('%B %d, %Y') }" if first_date != second_date
136
+ messenger.info("Creating #{ first_date == second_date ? 'entry' : 'entries' }: #{ date_string }")
141
137
 
142
- # Upload any Day One entries to Dropbox (if there are any).
143
- unless Dir[Configuration::sifttter_redux[:dayone_local_filepath] + '/*'].empty?
144
- dbu.local_target = "#{ Configuration::sifttter_redux[:dayone_local_filepath] }/*"
145
- dbu.remote_target = Configuration::sifttter_redux[:dayone_remote_filepath]
146
- dbu.message = 'Uploading Day One entries to Dropbox...'
138
+ # Download Sifttter files from Dropbox.
139
+ dbu = SifttterRedux::DropboxUploader.new(configuration.db_uploader[:exe_filepath])
140
+ dbu.verbose = SifttterRedux.verbose
141
+ dbu.local_target = configuration.sifttter_redux[:sifttter_local_filepath]
142
+ dbu.remote_target = configuration.sifttter_redux[:sifttter_remote_filepath]
143
+ dbu.message = 'Downloading Sifttter files...'
147
144
 
148
- CLIMessage::info_block(dbu.message || dbu::DEFAULT_MESSAGE, 'Done.', SifttterRedux.verbose) do
149
- dbu.upload
150
- end
151
- end
145
+ messenger.info_block(dbu.message || dbu::DEFAULT_MESSAGE, 'Done.', SifttterRedux.verbose) do
146
+ dbu.download
147
+ end
152
148
 
153
- # Remove any downloaded local files that we no longer need.
154
- SifttterRedux::cleanup_temp_files
149
+ # Process a new Sifttter entry for each date.
150
+ dates.each do |date|
151
+ SifttterRedux::Sifttter.run(date)
155
152
  end
156
- rescue StandardError => e
157
- CLIMessage::error(e.to_s)
158
- exit!(1)
153
+
154
+ # Upload any Day One entries to Dropbox (if there are any).
155
+ unless Dir[configuration.sifttter_redux[:dayone_local_filepath] + '/*'].empty?
156
+ dbu.local_target = "#{ configuration.sifttter_redux[:dayone_local_filepath] }/*"
157
+ dbu.remote_target = configuration.sifttter_redux[:dayone_remote_filepath]
158
+ dbu.message = 'Uploading Day One entries to Dropbox...'
159
+
160
+ messenger.info_block(dbu.message || SifttterRedux::DEFAULT_MESSAGE, 'Done.', SifttterRedux.verbose) do
161
+ dbu.upload
162
+ end
163
+ end
164
+
165
+ # Remove any downloaded local files that we no longer need.
166
+ SifttterRedux.cleanup_temp_files
159
167
  end
160
168
  end
161
169
 
@@ -164,22 +172,25 @@ end
164
172
  # ------------------------------------------------------
165
173
  # init command
166
174
  #
167
- # Initializes the script.
175
+ # Initializes the app by asking the user for information
176
+ # needed torun.
168
177
  # ------------------------------------------------------
169
- desc 'Install and SifttterRedux::initialize dependencies'
178
+ desc 'Install and initialize dependencies'
170
179
  command :init do |c|
171
180
  c.switch([:s], desc: 'Run init from scratch (i.e., clear out all values from configuration)')
172
-
173
181
  c.action do |global_options, options, args|
174
- CLIMessage::section_block('INITIALIZING...') do
182
+ messenger.section_block('INITIALIZING...') do
175
183
  if options[:s]
176
184
  SifttterRedux::init(true)
177
185
  else
178
186
  long_message = "You've already initialized Sifttter Redux. Do it again?"
179
- SifttterRedux::init if CLIMessage::prompt(long_message, 'N').downcase == 'y'
187
+ SifttterRedux::init if messenger.prompt(long_message, 'N').downcase == 'y'
180
188
  end
181
189
  end
182
190
  end
183
191
  end
184
192
 
193
+ # ======================================================
194
+ # Run!
195
+ # ======================================================
185
196
  exit run(ARGV)
@@ -0,0 +1,7 @@
1
+ Feature: UI
2
+ As a user, when I ask for help, I should be presented
3
+ with instructions on how to run the app.
4
+
5
+ Scenario: Display help instructions
6
+ When I get help for "srd"
7
+ Then the exit status should be 0
@@ -0,0 +1,130 @@
1
+ @announce
2
+ Feature: Initialization
3
+ As a user, when I initialize Sifttter Redux,
4
+ I should be guided through the process as
5
+ necessary.
6
+
7
+ Scenario: Basic Initialization
8
+ Given no file located at "/tmp/srd/.sifttter_redux"
9
+ And an empty file located at "/tmp/srd/.dropbox_uploader"
10
+ When I run `srd init` interactively
11
+ And I type ""
12
+ And I type "~/sifttter_download"
13
+ And I type "/Apps/ifttt/Sifttter"
14
+ And I type "~/day_one_download"
15
+ And I type "/Apps/Day\ One/Journal.dayone/entries"
16
+ Then the exit status should be 0
17
+ And the file "/tmp/srd/.sifttter_redux" should contain:
18
+ """
19
+ ---
20
+ :sifttter_redux:
21
+ :config_location: "/tmp/srd/.sifttter_redux"
22
+ :log_level: WARN
23
+ """
24
+ And the file "/tmp/srd/.sifttter_redux" should contain:
25
+ """
26
+ :sifttter_local_filepath: "/tmp/srd/sifttter_download"
27
+ :sifttter_remote_filepath: "/Apps/ifttt/Sifttter"
28
+ :dayone_local_filepath: "/tmp/srd/day_one_download"
29
+ :dayone_remote_filepath: "/Apps/Day\\ One/Journal.dayone/entries"
30
+ :db_uploader:
31
+ :base_filepath: "/usr/local/opt"
32
+ :dbu_filepath: "/usr/local/opt/Dropbox-Uploader"
33
+ :exe_filepath: "/usr/local/opt/Dropbox-Uploader/dropbox_uploader.sh"
34
+ """
35
+
36
+ Scenario: Reinitialization (refuse)
37
+ Given a file located at "/tmp/srd/.sifttter_redux" with the contents:
38
+ """
39
+ ---
40
+ :sifttter_redux:
41
+ :config_location: "/tmp/srd/.sifttter_redux"
42
+ :log_level: WARN
43
+ :version: 0.5.4
44
+ :sifttter_local_filepath: "/tmp/srd/sifttter_download"
45
+ :sifttter_remote_filepath: "/Apps/ifttt/Sifttter"
46
+ :dayone_local_filepath: "/tmp/srd/day_one_download"
47
+ :dayone_remote_filepath: "/Apps/Day\\ One/Journal.dayone/entries"
48
+ :db_uploader:
49
+ :base_filepath: "/usr/local/opt"
50
+ :dbu_filepath: "/usr/local/opt/Dropbox-Uploader"
51
+ :exe_filepath: "/usr/local/opt/Dropbox-Uploader/dropbox_uploader.sh"
52
+ """
53
+ And an empty file located at "/tmp/srd/.dropbox_uploader"
54
+ When I run `srd init` interactively
55
+ And I type ""
56
+ Then the exit status should be 0
57
+
58
+ Scenario: Reinitialization (accept)
59
+ Given a file located at "/tmp/srd/.sifttter_redux" with the contents:
60
+ """
61
+ ---
62
+ :sifttter_redux:
63
+ :config_location: "/tmp/srd/.sifttter_redux"
64
+ :log_level: WARN
65
+ :version: 0.5.4
66
+ :sifttter_local_filepath: "/tmp/srd/sifttter_download"
67
+ :sifttter_remote_filepath: "/Apps/ifttt/Sifttter"
68
+ :dayone_local_filepath: "/tmp/srd/day_one_download"
69
+ :dayone_remote_filepath: "/Apps/Day\\ One/Journal.dayone/entries"
70
+ :db_uploader:
71
+ :base_filepath: "/usr/local/opt"
72
+ :dbu_filepath: "/usr/local/opt/Dropbox-Uploader"
73
+ :exe_filepath: "/usr/local/opt/Dropbox-Uploader/dropbox_uploader.sh"
74
+ """
75
+ And an empty file located at "/tmp/srd/.dropbox_uploader"
76
+ When I run `srd init` interactively
77
+ And I type "y"
78
+ And I type ""
79
+ And I type "~/sifttter_download2"
80
+ And I type "/Apps/ifttt/Sifttter2"
81
+ And I type "~/day_one_download2"
82
+ And I type "/Apps/Day\ One/Journal.dayone/entries2"
83
+ Then the exit status should be 0
84
+ And the file "/tmp/srd/.sifttter_redux" should contain:
85
+ """
86
+ ---
87
+ :sifttter_redux:
88
+ :config_location: "/tmp/srd/.sifttter_redux"
89
+ :log_level: WARN
90
+ """
91
+ And the file "/tmp/srd/.sifttter_redux" should contain:
92
+ """
93
+ :sifttter_local_filepath: "/tmp/srd/sifttter_download2"
94
+ :sifttter_remote_filepath: "/Apps/ifttt/Sifttter2"
95
+ :dayone_local_filepath: "/tmp/srd/day_one_download2"
96
+ :dayone_remote_filepath: "/Apps/Day\\ One/Journal.dayone/entries2"
97
+ :db_uploader:
98
+ :base_filepath: "/usr/local/opt"
99
+ :dbu_filepath: "/usr/local/opt/Dropbox-Uploader"
100
+ :exe_filepath: "/usr/local/opt/Dropbox-Uploader/dropbox_uploader.sh"
101
+ """
102
+
103
+ Scenario: Reinitialization (from scratch)
104
+ Given no file located at "/tmp/srd/.sifttter_redux"
105
+ And an empty file located at "/tmp/srd/.dropbox_uploader"
106
+ When I run `srd init -s` interactively
107
+ And I type ""
108
+ And I type "~/sifttter_download"
109
+ And I type "/Apps/ifttt/Sifttter"
110
+ And I type "~/day_one_download"
111
+ And I type "/Apps/Day\ One/Journal.dayone/entries"
112
+ Then the exit status should be 0
113
+ And the file "/tmp/srd/.sifttter_redux" should contain:
114
+ """
115
+ ---
116
+ :sifttter_redux:
117
+ :config_location: "/tmp/srd/.sifttter_redux"
118
+ :log_level: WARN
119
+ """
120
+ And the file "/tmp/srd/.sifttter_redux" should contain:
121
+ """
122
+ :sifttter_local_filepath: "/tmp/srd/sifttter_download"
123
+ :sifttter_remote_filepath: "/Apps/ifttt/Sifttter"
124
+ :dayone_local_filepath: "/tmp/srd/day_one_download"
125
+ :dayone_remote_filepath: "/Apps/Day\\ One/Journal.dayone/entries"
126
+ :db_uploader:
127
+ :base_filepath: "/usr/local/opt"
128
+ :dbu_filepath: "/usr/local/opt/Dropbox-Uploader"
129
+ :exe_filepath: "/usr/local/opt/Dropbox-Uploader/dropbox_uploader.sh"
130
+ """