cmdline-sub 0.1.1 → 0.1.3

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.
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