cmdline-sub 0.1.1 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/README +13 -5
  3. data/Rakefile +1 -0
  4. data/bin/sub +90 -81
  5. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2fd227736fa0bc8d59c84b1909f3c7abfd63ec86325a855ae5e4e2302693f35b
4
- data.tar.gz: 84c336782e6d58eddba82d06874809d22074c968fd94a82c61a88043d922bb52
3
+ metadata.gz: c389766a40ec1f0894ff39ae9156d8ab1f737e32da5e6a83ac82e758fd420e06
4
+ data.tar.gz: cbe56894e3d43b63ec14da75db54b820c9347a197e32c9ae6a5cc649ddabdb1c
5
5
  SHA512:
6
- metadata.gz: e1955cee9482f5b650924d6302185519553bc82dc9959b28449e57c5bb2f5527e2553eb18be60fe5a7f8cb9d752d5baf68709bab3448b66dfda3d196b2ad51e5
7
- data.tar.gz: 9435447e39679f02f4e2365faa2c01cd2b7e9fb3ca88388e5d894b913c6edf5885fde6a32504e85cd4da29d75a8c0395ae265dbdd3d5917265ca4fc3dd4116e8
6
+ metadata.gz: 71806a610d4b0a4e3e14e6ed75ab31f0dc4d1c34ac72767b3b8e063ed139322685f1c0585929f58f447a25e07f717171892d7ed35ace82f4a8d67ad9dd00b394
7
+ data.tar.gz: d08f7dd174f7546d4b451a5ee6a0639eea308ca894db10c861dbd3556efbe6965770c2529a822da2a64821de52a68444d3771fa253a9e6f46ba17923e65b77fb
data/README CHANGED
@@ -1,8 +1,8 @@
1
1
  Sub
2
2
  ===
3
3
 
4
- sub v0.1.0
5
- Usage: sub COMMANDLINE -- (PATTERN/SUBSTITUTION[/SUB_OPTIONS])+ (/GLOBAL_OPTIONS)?
4
+ sub v0.1.3
5
+ Usage: sub COMMANDLINE -- (PATTERN/SUBSTITUTION[/SUB_FLAGS])+ (/GLOBAL_FLAGS)?
6
6
 
7
7
  sub substitutes the matching pattern in every word in the command line with the
8
8
  substitution. Only the first matched pattern in the word is substituted unless
@@ -53,16 +53,22 @@ Substitution flags:
53
53
  ex: sub cp here.txt there.txt -- CP/mv/i #=> mv here.txt there.txt
54
54
  -g: General substitution: substitute all matches in the word, not just the first
55
55
  ex: sub cp here.txt there.txt -- ./_/g #=> __ ________ _________
56
- -e: Expand * in commandline after replacements are made
57
- ex: sub ls mydir -- mydir/*/e #=> ls bin README
56
+ -r: Don't escape substituted value, allow shell to expand and split it
57
+ ex: sub ls mydir -- 'mydir/*/r' #=> ls bin README
58
58
  Global flags:
59
- -p: Print the command instead of executing it
59
+ -p: Print the command instead of executing it (adds newline)
60
+ -P: Print the command instead of executing it (doesn't add newline)
60
61
  -c: Copy the command to clipboard instead of executing it
61
62
  -I: Set interactive mode: shows the command and asks if you want to execute it
62
63
  -v: Set verbose mode
63
64
  -D: Set debug mode
65
+ Other flags:
66
+ -h,--help: Show help (-h shows short help, --help shows full)
67
+ -v,--version: Show version followed by a newline
64
68
 
65
69
  Install:
70
+ gem install cmdline-sub
71
+ or:
66
72
  cd $HOME
67
73
  git clone git@github.com:luke-gru/sub.git
68
74
  cd sub
@@ -71,6 +77,8 @@ Install:
71
77
  # INSTALL_PREFIX="/bin" sudo rake install
72
78
 
73
79
  Uninstall:
80
+ gem uninstall cmdline-sub
81
+ or:
74
82
  cd $HOME/sub
75
83
  sudo rake uninstall # removes /usr/local/bin/sub
76
84
  # or
data/Rakefile CHANGED
@@ -57,6 +57,7 @@ end
57
57
 
58
58
  desc "Uninstall (removes /usr/local/bin/sub or $(INSTALL_PREFIX)/sub). Use sudo if necessary."
59
59
  task :uninstall do
60
+ require "fileutils"
60
61
  install_dir = ENV["INSTALL_PREFIX"] || "/usr/local/bin"
61
62
  unless File.directory?(install_dir)
62
63
  $stderr.puts "Directory #{install_dir} does not exist. Uninstall failed."
data/bin/sub CHANGED
@@ -2,10 +2,15 @@
2
2
  # vim: set ft=ruby
3
3
  # frozen_string_literal: true
4
4
 
5
- VERSION = "0.1.1"
5
+ if ENV["TESTING_SUB_CMD"] == "1" && ENV["DEBUGGING_SUB_CMD"] == "1"
6
+ require "debug"
7
+ end
8
+ require "shellwords"
9
+
10
+ VERSION = "0.1.3"
6
11
 
7
12
  USAGE = <<EOF
8
- Usage: sub COMMANDLINE -- (PATTERN/SUBSTITUTION[/SUB_OPTIONS])+ (/GLOBAL_OPTIONS)?
13
+ Usage: sub COMMANDLINE -- (PATTERN/SUBSTITUTION[/SUB_FLAGS])+ (/GLOBAL_FLAGS)?
9
14
 
10
15
  sub substitutes the matching pattern in every word in the command line with the
11
16
  substitution. Only the first matched pattern in the word is substituted unless
@@ -46,6 +51,8 @@ EOF
46
51
 
47
52
  INSTALLATION = <<EOF
48
53
  Install:
54
+ \t gem install cmdline-sub
55
+ or:
49
56
  \tcd $HOME
50
57
  \tgit clone git@github.com:luke-gru/sub.git
51
58
  \tcd sub
@@ -56,6 +63,8 @@ EOF
56
63
 
57
64
  UNINSTALLATION = <<EOF
58
65
  Uninstall:
66
+ \t gem uninstall cmdline-sub
67
+ or:
59
68
  \tcd $HOME/sub
60
69
  \tsudo rake uninstall # removes /usr/local/bin/sub
61
70
  \t# or
@@ -64,7 +73,7 @@ EOF
64
73
 
65
74
  LICENSE = "License: MIT"
66
75
 
67
- FLAGS = [
76
+ SUB_FLAGS = [
68
77
  # per substitution flags
69
78
  { flag: 'f', name: :first_match, desc: "Substitute first matching word only.",
70
79
  example: "sub wget https://wget.com -- wget/curl/f #=> curl https://wget.com",
@@ -81,10 +90,11 @@ FLAGS = [
81
90
  { flag: 'g', name: :general, desc: "General substitution: substitute all matches in the word, not just the first",
82
91
  example: "sub cp here.txt there.txt -- ./_/g #=> __ ________ _________",
83
92
  },
84
- { flag: 'e', name: :expand_star, desc: "Expand * in commandline after replacements are made",
85
- example: [ "sub ls mydir -- mydir/*/e #=> ls bin README" ]
93
+ { flag: 'r', name: :raw_mode, desc: "Don't escape substituted value, allow shell to expand and split it",
94
+ example: [ "sub ls mydir -- 'mydir/*/r' #=> ls bin README" ]
86
95
  },
87
- nil, # separator
96
+ ]
97
+ GLOBAL_FLAGS = [
88
98
  # global flags
89
99
  { flag: 'p', name: :print_only, desc: "Print the command instead of executing it (adds newline)" },
90
100
  { flag: 'P', name: :print_only_no_newline, desc: "Print the command instead of executing it (doesn't add newline)" },
@@ -93,28 +103,26 @@ FLAGS = [
93
103
  { flag: 'v', name: :verbose, desc: "Set verbose mode" },
94
104
  { flag: 'D', name: :debug, desc: "Set debug mode" },
95
105
  ]
96
- GLOBAL_FLAGS_MAP = {
97
- print_only: 'p',
98
- print_only_no_newline: 'P',
99
- copy_to_clipboard: 'c',
100
- interactive: 'I',
101
- verbose: 'v',
102
- debug: 'D',
103
- }
104
- GLOBAL_FLAGS = GLOBAL_FLAGS_MAP.values
106
+ OTHER_FLAGS = [
107
+ { flag: ['h', '--help'], name: :help, desc: "Show help (-h shows short help, --help shows full)", },
108
+ { flag: ['v', '--version'], name: :version, desc: "Show version followed by a newline" },
109
+ ]
110
+ FLAGS = SUB_FLAGS + GLOBAL_FLAGS + OTHER_FLAGS
111
+ FLAGS_BY_NAME = FLAGS.map do |f|
112
+ { f[:name] => f[:flag] }
113
+ end.inject(Hash.new) { |memo, h| memo.merge(h) }
114
+ GLOBAL_FLAGS_BY_NAME = GLOBAL_FLAGS.map do |f|
115
+ { f[:name] => f[:flag] }
116
+ end.inject(Hash.new) { |memo, h| memo.merge(h) }
117
+ RAW_GLOBAL_FLAGS = GLOBAL_FLAGS_BY_NAME.values
105
118
 
106
119
  def print_help(full: true)
107
120
  output = String.new
108
121
  output << "sub v#{VERSION}" << "\n"
109
122
  output << USAGE << "\n"
110
- output << "Flags:\n\n"
111
- output << "Substitution flags:\n"
112
- FLAGS.each do |fhash|
113
- if fhash.nil?
114
- output << "Global flags:\n"
115
- next
116
- end
117
- output << "-#{fhash[:flag]}:\t#{fhash[:desc]}\n"
123
+ output_flag_info = lambda do |fhash|
124
+ flags = Array(fhash[:flag])
125
+ output << "-#{flags.join(',')}:\t#{fhash[:desc]}\n"
118
126
  if ex = fhash[:example]
119
127
  case ex
120
128
  when String
@@ -126,6 +134,13 @@ def print_help(full: true)
126
134
  end
127
135
  end
128
136
  end
137
+ output << "Flags:\n\n"
138
+ output << "Substitution flags:\n"
139
+ SUB_FLAGS.each(&output_flag_info)
140
+ output << "Global flags:\n"
141
+ GLOBAL_FLAGS.each(&output_flag_info)
142
+ output << "Other flags:\n"
143
+ OTHER_FLAGS.each(&output_flag_info)
129
144
  if full
130
145
  output << "\n" << INSTALLATION
131
146
  output << "\n" << UNINSTALLATION
@@ -142,11 +157,8 @@ def parse_argv_and_sub_strs(argv)
142
157
  ret_argv = []
143
158
  sub_strs = []
144
159
  if argv.size == 0
145
- require "readline"
146
- line = Readline.readline "cmd: ", true
147
- ret_argv.replace line.strip.split(/\s+/)
148
- line = Readline.readline "pat/sub[/options]: ", true
149
- sub_strs << line.strip
160
+ print_help(full: true)
161
+ exit 1
150
162
  else
151
163
  last_dashdash_idx = argv.rindex('--')
152
164
  argv.each_with_index do |arg, i|
@@ -178,26 +190,28 @@ end
178
190
 
179
191
  argv, sub_strs = parse_argv_and_sub_strs(ARGV)
180
192
 
181
- def flag(flags_str, char)
193
+ def parse_simple_flag!(flags_str, char)
182
194
  flag = flags_str.include?(char)
183
195
  flags_str.delete!(char)
184
196
  flag
185
197
  end
186
198
 
187
199
  def flag_by_name(name)
188
- FLAGS.each do |fhash|
189
- next if fhash.nil?
190
- if fhash[:name] == name
191
- return fhash[:flag]
192
- end
193
- end
194
- raise ArgumentError, "bad flag: #{name}"
200
+ FLAGS_BY_NAME.fetch(name)
195
201
  end
196
202
 
197
- def raw_global_options?(raw_flags)
203
+ def raw_global_flags?(raw_flags)
198
204
  raw_flags = raw_flags.strip
199
205
  return false if raw_flags.empty?
200
- raw_flags.gsub(Regexp.new(GLOBAL_FLAGS.join('|')), '').empty?
206
+ raw_flags.gsub(Regexp.new(RAW_GLOBAL_FLAGS.join('|')), '').empty?
207
+ end
208
+
209
+ def parse_global_flag!(name, flags, global_flags)
210
+ if global_flags[name]
211
+ flags.delete!(GLOBAL_FLAGS_BY_NAME[name])
212
+ else
213
+ global_flags[name] = parse_simple_flag!(flags, flag_by_name(name))
214
+ end
201
215
  end
202
216
 
203
217
  def parse_sub_strs(sub_strs)
@@ -205,11 +219,11 @@ def parse_sub_strs(sub_strs)
205
219
  pats = []
206
220
  subs = []
207
221
  match_flags = []
208
- global_flags = GLOBAL_FLAGS_MAP.transform_values { nil }
222
+ global_flags = GLOBAL_FLAGS_BY_NAME.transform_values { nil }
209
223
  raw_flags = []
210
224
  sub_strs.each_with_index do |sub_str, sub_i|
211
225
  # global flags at end of substitutions list, starts with /, ex: /o
212
- if sub_strs[sub_i+1].nil? && sub_str[0] == '/' && raw_global_options?(sub_str[1..-1])
226
+ if sub_strs[sub_i+1].nil? && sub_str[0] == '/' && raw_global_flags?(sub_str[1..-1])
213
227
  flags = sub_str[1..-1]
214
228
  else
215
229
  pat, sub, flags = sub_str.split(/(?<!\\)\//)
@@ -219,48 +233,27 @@ def parse_sub_strs(sub_strs)
219
233
  flags.strip!
220
234
  raw_flags << flags.dup
221
235
  flags_hash = {
222
- first_match: flag(flags, flag_by_name(:first_match)),
223
- last_match: flag(flags, flag_by_name(:last_match)),
224
- expand_star: flag(flags, flag_by_name(:expand_star)),
225
- literal: flag(flags, flag_by_name(:literal)),
226
- ignorecase: flag(flags, flag_by_name(:ignorecase)),
227
- general: flag(flags, flag_by_name(:general)),
236
+ first_match: parse_simple_flag!(flags, flag_by_name(:first_match)),
237
+ last_match: parse_simple_flag!(flags, flag_by_name(:last_match)),
238
+ raw_mode: parse_simple_flag!(flags, flag_by_name(:raw_mode)),
239
+ literal: parse_simple_flag!(flags, flag_by_name(:literal)),
240
+ ignorecase: parse_simple_flag!(flags, flag_by_name(:ignorecase)),
241
+ general: parse_simple_flag!(flags, flag_by_name(:general)),
228
242
  }
243
+ if flags_hash[:raw_mode]
244
+ flags_hash[:raw_mode] = 0 # bitfield
245
+ end
229
246
  pats << pat
230
247
  subs << sub
231
248
  match_flags << flags_hash
232
249
  end
233
250
 
234
- if global_flags[:interactive]
235
- flags.delete!(GLOBAL_FLAGS_MAP[:interactive])
236
- else
237
- global_flags[:interactive] = flag(flags, flag_by_name(:interactive)) || ARGV.size == 0
238
- end
239
- if global_flags[:print_only]
240
- flags.delete!(GLOBAL_FLAGS_MAP[:print_only])
241
- else
242
- global_flags[:print_only] = flag(flags, flag_by_name(:print_only))
243
- end
244
- if global_flags[:print_only_no_newline]
245
- flags.delete!(GLOBAL_FLAGS_MAP[:print_only_no_newline])
246
- else
247
- global_flags[:print_only_no_newline] = flag(flags, flag_by_name(:print_only_no_newline))
248
- end
249
- if global_flags[:copy_to_clipboard]
250
- flags.delete!(GLOBAL_FLAGS_MAP[:copy_to_clipboard])
251
- else
252
- global_flags[:copy_to_clipboard] = flag(flags, flag_by_name(:copy_to_clipboard))
253
- end
254
- if global_flags[:verbose]
255
- flags.delete!(GLOBAL_FLAGS_MAP[:verbose])
256
- else
257
- global_flags[:verbose] = flag(flags, flag_by_name(:verbose))
258
- end
259
- if global_flags[:debug]
260
- flags.delete!(GLOBAL_FLAGS_MAP[:debug])
261
- else
262
- global_flags[:debug] = flag(flags, flag_by_name(:debug))
263
- end
251
+ parse_global_flag!(:interactive, flags, global_flags)
252
+ parse_global_flag!(:print_only, flags, global_flags)
253
+ parse_global_flag!(:print_only_no_newline, flags, global_flags)
254
+ parse_global_flag!(:copy_to_clipboard, flags, global_flags)
255
+ parse_global_flag!(:verbose, flags, global_flags)
256
+ parse_global_flag!(:debug, flags, global_flags)
264
257
  unless flags.empty?
265
258
  $stderr.puts "Warning: unknown flag#{'s' if flags.size != 1}: #{flags}"
266
259
  end
@@ -272,7 +265,7 @@ end
272
265
  pats, subs, flags_hashes, raw_flags, global_flags = parse_sub_strs(sub_strs)
273
266
 
274
267
  if pats.empty? && flags_hashes.empty? && global_flags.empty?
275
- $stderr.puts "Incorrect substitution, format is: pattern/substitution[/options]"
268
+ $stderr.puts "Incorrect substitution, format is: pattern/substitution[/flags]"
276
269
  exit 1
277
270
  end
278
271
 
@@ -314,6 +307,9 @@ def argv_replace!(argv, regexps, subs, flags)
314
307
  scan_size = arg.scan(regexp).size
315
308
  scan_size = 1 if scan_size > 1
316
309
  end
310
+ if flags[i].fetch(:raw_mode)
311
+ flags[i][:raw_mode] |= i+1
312
+ end
317
313
  if new_arg != arg || new_arg == subs[i]
318
314
  num_replacements += scan_size
319
315
  end
@@ -370,7 +366,7 @@ def copy!(cmd_line, global_flags)
370
366
  end
371
367
  end
372
368
 
373
- def exec_cmd(argv, regexps, subs, raw_flags, num_replacements, global_flags)
369
+ def exec_cmd(argv, regexps, subs, flags_hashes, raw_flags, global_flags, num_replacements)
374
370
  if global_flags.fetch(:debug)
375
371
  puts "Patterns: #{regexps.inspect}"
376
372
  puts "Substitutions: #{subs.inspect}"
@@ -393,11 +389,24 @@ def exec_cmd(argv, regexps, subs, raw_flags, num_replacements, global_flags)
393
389
  end
394
390
 
395
391
  cmd = argv.shift
396
- cmd_line = "#{cmd} #{argv.join(' ')}"
392
+ argv_string = String.new
393
+ argv.each_with_index do |arg, i|
394
+ raw_mode = flags_hashes.any? { |flags_hash| flags_hash[:raw_mode] && (flags_hash[:raw_mode] & i+1) != 0 }
395
+ if raw_mode
396
+ argv_string << arg
397
+ else
398
+ argv_string << Shellwords.escape(arg)
399
+ end
400
+ unless argv[i+1].nil?
401
+ argv_string << " "
402
+ end
403
+ end
404
+ cmd_line = "#{cmd} #{argv_string}"
397
405
  action = global_flags.fetch(:copy_to_clipboard) ? "copy" : "execute"
398
406
  if global_flags.fetch(:interactive) && !global_flags.fetch(:print_only)
399
407
  $stdout.puts "Would you like to #{action} the following command? [y(es),n(o)]"
400
408
  $stdout.puts cmd_line
409
+ $stdout.flush
401
410
  ans = $stdin.gets().strip
402
411
  if ans !~ /y(es)?/i # treat as no
403
412
  exit 1
@@ -411,7 +420,7 @@ def exec_cmd(argv, regexps, subs, raw_flags, num_replacements, global_flags)
411
420
  copy!(cmd_line, global_flags) if global_flags.fetch(:copy_to_clipboard)
412
421
  exit 0 if global_flags.fetch(:print_only) || global_flags.fetch(:print_only_no_newline) || global_flags.fetch(:copy_to_clipboard)
413
422
  begin
414
- exec cmd, *argv
423
+ exec cmd_line
415
424
  rescue SystemCallError => e
416
425
  if e.class == Errno::ENOENT
417
426
  # act like a shell
@@ -423,4 +432,4 @@ def exec_cmd(argv, regexps, subs, raw_flags, num_replacements, global_flags)
423
432
  end
424
433
  end
425
434
 
426
- exec_cmd(argv, regexps, subs, raw_flags, num_replacements, global_flags)
435
+ exec_cmd(argv, regexps, subs, flags_hashes, raw_flags, global_flags, num_replacements)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cmdline-sub
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Luke Gruber
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-11-20 00:00:00.000000000 Z
11
+ date: 2024-12-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: debug