linux-command 0.2.2__tar.gz → 0.3.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: linux-command
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: A command line tool to perform custom tasks.
5
5
  Home-page: https://github.com/MouxiaoHuang/linux-command
6
6
  Author: Mouxiao Huang
@@ -11,6 +11,15 @@ Classifier: Operating System :: OS Independent
11
11
  Requires-Python: >=3.6
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
+ Dynamic: author
15
+ Dynamic: author-email
16
+ Dynamic: classifier
17
+ Dynamic: description
18
+ Dynamic: description-content-type
19
+ Dynamic: home-page
20
+ Dynamic: license-file
21
+ Dynamic: requires-python
22
+ Dynamic: summary
14
23
 
15
24
 
16
25
  # linux-command
@@ -25,6 +34,16 @@ To install the package, run the following command:
25
34
  pip install linux-command
26
35
  ```
27
36
 
37
+ ## Install From Source (Development)
38
+
39
+ If you want to develop or modify the tool locally:
40
+
41
+ ```bash
42
+ git clone https://github.com/MouxiaoHuang/linux-command.git
43
+ cd linux-command
44
+ pip install -e .
45
+ ```
46
+
28
47
  ## Usage
29
48
 
30
49
  Once installed, you can access all commands using `cmd` followed by the specific command name. You can use `cmd -h` or `cmd --help` to see the supported commands. Below is a list of supported commands along with examples for each.
@@ -155,6 +174,12 @@ cmd ps-grep python
155
174
  cmd kill process_name
156
175
  ```
157
176
 
177
+ To force kill:
178
+
179
+ ```bash
180
+ cmd kill --force process_name
181
+ ```
182
+
158
183
  ---
159
184
 
160
185
  ### 21. `df` - Show disk usage in human-readable format
@@ -163,7 +188,7 @@ cmd kill process_name
163
188
  cmd df
164
189
  ```
165
190
 
166
- ### 22. `du [path]` - Show disk usage for a specific file or directory
191
+ ### 22. `du [path]` - Show disk usage for a specific file or directory (default: current directory)
167
192
 
168
193
  ```bash
169
194
  cmd du /path/to/directory
@@ -256,6 +281,21 @@ This command will create a `.zip` file at `/path/to/output.zip` that contains ev
256
281
 
257
282
  ---
258
283
 
284
+ ## Aliases
285
+
286
+ These commands are aliases for the same behavior:
287
+
288
+ - `lsf` = `ls-file`
289
+ - `lsd` = `ls-dir`
290
+ - `ls-bs` = `ls-block-size`
291
+ - `disk` = `du`
292
+ - `tar` = `tar-compress`
293
+ - `untar` = `tar-extract`
294
+ - `zip` = `zip-compress`
295
+ - `zip-all` = `zip-compress`
296
+ - `unzip` = `unzip-all`
297
+ - `convert-vid` = `convert-video`
298
+
259
299
  ## Contributing
260
300
 
261
301
  We welcome contributions from the community! If you'd like to help improve `linux-command`, feel free to report issues or submit pull requests.
@@ -1,17 +1,3 @@
1
- Metadata-Version: 2.1
2
- Name: linux-command
3
- Version: 0.2.2
4
- Summary: A command line tool to perform custom tasks.
5
- Home-page: https://github.com/MouxiaoHuang/linux-command
6
- Author: Mouxiao Huang
7
- Author-email: huangmouxiao@gmail.com
8
- Classifier: Programming Language :: Python :: 3
9
- Classifier: License :: OSI Approved :: MIT License
10
- Classifier: Operating System :: OS Independent
11
- Requires-Python: >=3.6
12
- Description-Content-Type: text/markdown
13
- License-File: LICENSE
14
-
15
1
 
16
2
  # linux-command
17
3
 
@@ -25,6 +11,16 @@ To install the package, run the following command:
25
11
  pip install linux-command
26
12
  ```
27
13
 
14
+ ## Install From Source (Development)
15
+
16
+ If you want to develop or modify the tool locally:
17
+
18
+ ```bash
19
+ git clone https://github.com/MouxiaoHuang/linux-command.git
20
+ cd linux-command
21
+ pip install -e .
22
+ ```
23
+
28
24
  ## Usage
29
25
 
30
26
  Once installed, you can access all commands using `cmd` followed by the specific command name. You can use `cmd -h` or `cmd --help` to see the supported commands. Below is a list of supported commands along with examples for each.
@@ -155,6 +151,12 @@ cmd ps-grep python
155
151
  cmd kill process_name
156
152
  ```
157
153
 
154
+ To force kill:
155
+
156
+ ```bash
157
+ cmd kill --force process_name
158
+ ```
159
+
158
160
  ---
159
161
 
160
162
  ### 21. `df` - Show disk usage in human-readable format
@@ -163,7 +165,7 @@ cmd kill process_name
163
165
  cmd df
164
166
  ```
165
167
 
166
- ### 22. `du [path]` - Show disk usage for a specific file or directory
168
+ ### 22. `du [path]` - Show disk usage for a specific file or directory (default: current directory)
167
169
 
168
170
  ```bash
169
171
  cmd du /path/to/directory
@@ -256,6 +258,21 @@ This command will create a `.zip` file at `/path/to/output.zip` that contains ev
256
258
 
257
259
  ---
258
260
 
261
+ ## Aliases
262
+
263
+ These commands are aliases for the same behavior:
264
+
265
+ - `lsf` = `ls-file`
266
+ - `lsd` = `ls-dir`
267
+ - `ls-bs` = `ls-block-size`
268
+ - `disk` = `du`
269
+ - `tar` = `tar-compress`
270
+ - `untar` = `tar-extract`
271
+ - `zip` = `zip-compress`
272
+ - `zip-all` = `zip-compress`
273
+ - `unzip` = `unzip-all`
274
+ - `convert-vid` = `convert-video`
275
+
259
276
  ## Contributing
260
277
 
261
278
  We welcome contributions from the community! If you'd like to help improve `linux-command`, feel free to report issues or submit pull requests.
@@ -26,27 +26,35 @@ SOFTWARE.
26
26
  For more information, visit the project page: https://github.com/MouxiaoHuang/linux-command
27
27
  """
28
28
  import argparse
29
- import os
30
29
  import glob
30
+ import os
31
+ import shutil
32
+ import signal
33
+ import subprocess
34
+ from fnmatch import fnmatch
31
35
 
32
36
 
33
37
  # Define the version
34
- VERSION = "0.2.2"
38
+ VERSION = "0.3.0"
35
39
  PROJECT_URL = "https://github.com/MouxiaoHuang/linux-command"
36
40
 
37
41
 
38
42
  # Command descriptions
39
43
  commands = {
40
44
  'ls': 'List contents.',
45
+ 'ls-all': 'List all files, including hidden ones.',
41
46
  'lsf': 'Count all files or filter by a specified pattern, extension or keyword. Same as `ls-file`.',
42
47
  'ls-file': 'Count all files or filter by a specified pattern, extension or keyword. Same as `lsf`.',
43
48
  'lsd': 'Count all directories. Same as `ls-dir`.',
44
49
  'ls-dir': 'Count all directories. Same as `lsd`.',
45
50
  'ls-reverse': 'List files and directories in reverse order.',
46
51
  'ls-time': 'List sorted by modification time, newest first.',
52
+ 'ls-human': 'List in human-readable format (for file sizes)',
53
+ 'ls-long': 'Long format listing',
54
+ 'ls-size': 'Sort files by size',
55
+ 'ls-recursive': 'Recursively list files in directories and subdirectories.',
47
56
  'ls-recursive-size': 'List all files and directories recursively, with sizes in human-readable format',
48
57
  'ls-bs': 'Display the size of each file in specified block size (e.g., K, M, G).',
49
- 'ls-size': 'Display the size of each file in specified block size (e.g., K, M, G).',
50
58
  'ls-block-size': 'Display the size of each file in specified block size (e.g., K, M, G).',
51
59
  'ps': 'Basic process list.',
52
60
  'ps-all': 'Show all processes.',
@@ -62,12 +70,13 @@ commands = {
62
70
  'rm': 'Remove file, directory, or multiple files by patterns (e.g., *.txt)',
63
71
  'grep': 'Search for a pattern in files or output.',
64
72
  'tar': 'Pack into .tar or .tar.gz file.',
65
- 'tar-comress': 'Pack into .tar or .tar.gz file.',
73
+ 'tar-compress': 'Pack into .tar or .tar.gz file.',
66
74
  'untar': 'Unpack .tar or .tar.gz file, or batch process in a directory.',
67
75
  'tar-extract': 'Unpack .tar or .tar.gz file, or batch process in a directory.',
68
76
  'tar-list': 'List all contents in a tar file.',
69
77
  'tar-add': 'Add a file to a tar file.',
70
78
  'zip': 'Pack a folder to a .zip file.',
79
+ 'zip-compress': 'Pack a folder to a .zip file.',
71
80
  'zip-all': 'Pack a folder to a .zip file.',
72
81
  'unzip': 'Unpack all .zip files in a directory to another.',
73
82
  'unzip-all': 'Unpack all .zip files in a directory to another.',
@@ -90,6 +99,130 @@ def confirm_action(message):
90
99
  return confirmation in ['yes', 'y']
91
100
 
92
101
 
102
+ def run_cmd(cmd_args):
103
+ """Run a command safely without invoking a shell."""
104
+ try:
105
+ subprocess.run(cmd_args, check=False)
106
+ except FileNotFoundError:
107
+ print(f"Command not found: {cmd_args[0]}")
108
+
109
+
110
+ def parse_excludes(args):
111
+ excludes = []
112
+ i = 0
113
+ while i < len(args):
114
+ arg = args[i]
115
+ if arg == '--exclude':
116
+ if i + 1 < len(args):
117
+ excludes.append(args[i + 1])
118
+ i += 2
119
+ else:
120
+ break
121
+ elif arg.startswith('--exclude='):
122
+ excludes.append(arg.split('=', 1)[1])
123
+ i += 1
124
+ else:
125
+ excludes.append(arg)
126
+ i += 1
127
+ return excludes
128
+
129
+
130
+ def expand_globs(args):
131
+ expanded = []
132
+ for arg in args:
133
+ if any(ch in arg for ch in ['*', '?', '[']):
134
+ matches = glob.glob(arg)
135
+ if matches:
136
+ expanded.extend(matches)
137
+ else:
138
+ expanded.append(arg)
139
+ else:
140
+ expanded.append(arg)
141
+ return expanded
142
+
143
+
144
+ def list_files_in_dir(path):
145
+ try:
146
+ return os.listdir(path)
147
+ except FileNotFoundError:
148
+ print(f"Path not found: {path}")
149
+ return []
150
+
151
+
152
+ def count_dirs(root):
153
+ total = 0
154
+ for _, dirs, _ in os.walk(root):
155
+ total += len(dirs)
156
+ return total
157
+
158
+
159
+ def count_files(root, pattern=None):
160
+ files = [f for f in list_files_in_dir(root) if os.path.isfile(os.path.join(root, f))]
161
+ if pattern is None:
162
+ return len(files)
163
+ if any(ch in pattern for ch in ['*', '?', '[']):
164
+ return sum(1 for f in files if fnmatch(f, pattern))
165
+ return sum(1 for f in files if pattern in f)
166
+
167
+
168
+ def filter_ps(keyword):
169
+ try:
170
+ result = subprocess.run(['ps', 'aux'], check=False, capture_output=True, text=True)
171
+ except FileNotFoundError:
172
+ print("Command not found: ps")
173
+ return []
174
+ lines = result.stdout.splitlines()
175
+ matches = []
176
+ for line in lines:
177
+ if keyword in line and 'ps aux' not in line:
178
+ matches.append(line)
179
+ return matches
180
+
181
+
182
+ def kill_processes(target, force=False):
183
+ sig = signal.SIGKILL if force else signal.SIGTERM
184
+ if target.isdigit():
185
+ try:
186
+ os.kill(int(target), sig)
187
+ print(f"Killed PID {target} with signal {sig}.")
188
+ except ProcessLookupError:
189
+ print(f"No such process: {target}")
190
+ except PermissionError:
191
+ print(f"Permission denied to kill PID {target}")
192
+ return
193
+
194
+ matches = filter_ps(target)
195
+ if not matches:
196
+ print(f"No processes found matching: {target}")
197
+ return
198
+
199
+ pids = []
200
+ for line in matches:
201
+ parts = line.split()
202
+ if len(parts) >= 2 and parts[1].isdigit():
203
+ pids.append(parts[1])
204
+
205
+ if not pids:
206
+ print(f"No processes found matching: {target}")
207
+ return
208
+
209
+ print("Matched processes:")
210
+ for line in matches:
211
+ print(line)
212
+
213
+ if confirm_action(f"Kill {len(pids)} process(es) matching '{target}'?"):
214
+ for pid in pids:
215
+ try:
216
+ os.kill(int(pid), sig)
217
+ except ProcessLookupError:
218
+ print(f"No such process: {pid}")
219
+ except PermissionError:
220
+ print(f"Permission denied to kill PID {pid}")
221
+ print("Kill completed.")
222
+ else:
223
+ print("Operation canceled.")
224
+
225
+
93
226
  def main():
94
227
  # Set up argparse
95
228
  parser = argparse.ArgumentParser(
@@ -102,11 +235,17 @@ def main():
102
235
  parser.add_argument('-V', '--version', action='store_true', help='Show program\'s version number and exit')
103
236
 
104
237
  # Main command and subcommands
105
- parser.add_argument('command', nargs='?', help='Command to execute')
106
- parser.add_argument('extra', nargs='*', help='Additional arguments for the command')
238
+ subparsers = parser.add_subparsers(dest='command')
239
+ for command, description in commands.items():
240
+ subparser = subparsers.add_parser(command, help=description, add_help=True)
241
+ subparser.add_argument('extra', nargs=argparse.REMAINDER, help='Additional arguments for the command')
107
242
 
108
243
  # Parse the arguments
109
- args = parser.parse_args()
244
+ args, unknown = parser.parse_known_args()
245
+ if unknown:
246
+ if not hasattr(args, 'extra') or args.extra is None:
247
+ args.extra = []
248
+ args.extra.extend(unknown)
110
249
 
111
250
  if args.help:
112
251
  custom_help()
@@ -115,116 +254,107 @@ def main():
115
254
  if args.version:
116
255
  print(f'linux-command {VERSION}')
117
256
  return
257
+
258
+ if args.command is None:
259
+ custom_help()
260
+ return
261
+
262
+ ls_simple = {
263
+ 'ls': [],
264
+ 'ls-all': ['-a'],
265
+ 'ls-reverse': ['-r'],
266
+ 'ls-time': ['-lt'],
267
+ 'ls-long': ['-l'],
268
+ 'ls-human': ['-lh'],
269
+ 'ls-recursive': ['-R'],
270
+ 'ls-recursive-size': ['-lRh'],
271
+ 'ls-size': ['-lS'],
272
+ }
273
+
274
+ ps_simple = {
275
+ 'ps': [],
276
+ 'ps-all': ['-A'],
277
+ 'ps-aux': ['aux'],
278
+ 'ps-sort-memory': ['aux', '--sort=-%mem'],
279
+ 'ps-sort-cpu': ['aux', '--sort=-%cpu'],
280
+ }
118
281
 
119
282
  # `ls` commands
120
- if args.command == 'ls':
121
- if len(args.extra) == 0:
122
- # List current directory
123
- os.system('ls')
124
- else:
125
- options = ' '.join(args.extra)
126
- os.system(f'ls {options}')
127
-
128
- # Advanced `ls` commands
283
+ if args.command in ls_simple:
284
+ run_cmd(['ls', *ls_simple[args.command], *expand_globs(args.extra)])
129
285
 
286
+ # Advanced `ls` commands
130
287
  elif args.command == 'ls-dir' or args.command == 'lsd':
131
288
  # Count the number of directories
132
- os.system('ls -lR | grep "^d" | wc -l')
289
+ print(count_dirs('.'))
133
290
 
134
291
  elif args.command == 'ls-file' or args.command == 'lsf':
135
292
  if len(args.extra) == 0:
136
293
  # Count the number of all files
137
- os.system('ls -l | grep "^-" | wc -l')
294
+ print(count_files('.'))
138
295
  else:
139
296
  # Count files of a specific type based on provided extension or pattern
140
297
  pattern = args.extra[0] # First extra argument as file pattern
141
- os.system(f'ls -l | grep "^-" | grep "{pattern}" | wc -l')
142
-
143
- elif args.command == 'ls-reverse':
144
- # List files and directories in reverse order
145
- if len(args.extra) == 0:
146
- os.system('ls -r')
147
- else:
148
- options = ' '.join(args.extra)
149
- os.system(f'ls -r {options}')
150
-
151
- elif args.command == 'ls-time':
152
- # Sort by modification time, newest first
153
- if len(args.extra) == 0:
154
- os.system('ls -lt')
155
- else:
156
- options = ' '.join(args.extra)
157
- os.system(f'ls -lt {options}')
298
+ print(count_files('.', pattern=pattern))
158
299
 
159
- elif args.command == 'ls-recursive-size':
160
- # List all files and directories recursively, with sizes in human-readable format
161
- if len(args.extra) == 0:
162
- os.system('ls -lRh')
163
- else:
164
- options = ' '.join(args.extra)
165
- os.system(f'ls -lRh {options}')
166
-
167
- elif args.command == 'ls-block-size' or args.command == 'ls-bs' or args.command == 'ls-size':
300
+ elif args.command == 'ls-block-size' or args.command == 'ls-bs':
168
301
  # Display the size of each file in specified block size
169
302
  if len(args.extra) == 1:
170
303
  block_size = args.extra[0]
171
- os.system(f'ls --block-size={block_size}')
304
+ run_cmd(['ls', f'--block-size={block_size}'])
172
305
  else:
173
306
  print('Please provide a valid block size (e.g., K, M, G).')
174
307
 
175
308
  # `ps` commands
176
- elif args.command == 'ps':
177
- # Basic process list
178
- os.system('ps')
179
-
180
- elif args.command == 'ps-all':
181
- # Show all processes
182
- os.system('ps -A')
309
+ elif args.command in ps_simple:
310
+ run_cmd(['ps', *ps_simple[args.command]])
183
311
 
184
312
  elif args.command == 'ps-user':
185
313
  # Show processes for a specific user
186
314
  if len(args.extra) > 0:
187
315
  user = args.extra[0]
188
- os.system(f'ps -u {user}')
316
+ run_cmd(['ps', '-u', user])
189
317
  else:
190
318
  print('Please provide a username to show processes for that user')
191
319
 
192
- elif args.command == 'ps-aux':
193
- # Show detailed information about all processes
194
- os.system('ps aux')
195
-
196
- elif args.command == 'ps-sort-memory':
197
- # Sort processes by memory usage
198
- os.system('ps aux --sort=-%mem')
199
-
200
- elif args.command == 'ps-sort-cpu':
201
- # Sort processes by CPU usage
202
- os.system('ps aux --sort=-%cpu')
203
-
204
320
  elif args.command == 'ps-grep':
205
321
  # Search for a specific process by name or keyword
206
322
  if len(args.extra) > 0:
207
323
  keyword = args.extra[0]
208
- os.system(f'ps aux | grep {keyword}')
324
+ matches = filter_ps(keyword)
325
+ for line in matches:
326
+ print(line)
209
327
  else:
210
328
  print('Please provide a keyword to search for in process list')
211
329
 
212
330
  # `kill` command
213
331
  elif args.command == 'kill':
214
- if len(args.extra) > 0:
215
- os.system(f'ps -ef | grep {args.extra[0]} | grep -v grep | cut -c 9-16 | xargs kill -9')
216
- else:
332
+ if len(args.extra) == 0:
217
333
  print('Please provide a process name or PID for kill command')
334
+ else:
335
+ force = False
336
+ extras = list(args.extra)
337
+ if '-9' in extras:
338
+ extras.remove('-9')
339
+ force = True
340
+ if '--force' in extras:
341
+ extras.remove('--force')
342
+ force = True
343
+ if len(extras) == 0:
344
+ print('Please provide a process name or PID for kill command')
345
+ return
346
+ target = extras[0]
347
+ kill_processes(target, force=force)
218
348
 
219
349
  # Disk usage and free space commands
220
350
  elif args.command == 'df':
221
- os.system('df -h')
351
+ run_cmd(['df', '-h'])
222
352
 
223
353
  elif args.command == 'du' or args.command == 'disk':
224
354
  if len(args.extra) > 0:
225
- os.system(f'du -sh {args.extra[0]}')
355
+ run_cmd(['du', '-sh', args.extra[0]])
226
356
  else:
227
- print('Please provide a valid path for du command')
357
+ run_cmd(['du', '-sh', '.'])
228
358
 
229
359
  # Remove files or directories with confirmation and support for bulk removal
230
360
  elif args.command == 'rm':
@@ -232,9 +362,12 @@ def main():
232
362
  target = args.extra[0]
233
363
  if confirm_action(f"Are you sure you want to remove '{target}'?"):
234
364
  if os.path.isdir(target):
235
- os.system(f'rm -rf {target}')
365
+ shutil.rmtree(target)
366
+ elif os.path.isfile(target):
367
+ os.remove(target)
236
368
  else:
237
- os.system(f'rm {target}')
369
+ print(f"Path not found: {target}")
370
+ return
238
371
  print(f"'{target}' has been removed.")
239
372
  else:
240
373
  print(f"Operation canceled. '{target}' was not removed.")
@@ -247,7 +380,10 @@ def main():
247
380
  print(f"Found {len(files_to_remove)} files to remove: {files_to_remove}")
248
381
  if confirm_action(f"Are you sure you want to remove these files?"):
249
382
  for file in files_to_remove:
250
- os.system(f'rm {file}')
383
+ if os.path.isdir(file):
384
+ shutil.rmtree(file)
385
+ elif os.path.isfile(file):
386
+ os.remove(file)
251
387
  print(f"Removed files matching {pattern}.")
252
388
  else:
253
389
  print(f"Operation canceled for files matching {pattern}.")
@@ -257,7 +393,7 @@ def main():
257
393
  # Search for a pattern in files or output
258
394
  elif args.command == 'grep':
259
395
  if len(args.extra) == 2:
260
- os.system(f'grep "{args.extra[0]}" {args.extra[1]}')
396
+ run_cmd(['grep', args.extra[0], args.extra[1]])
261
397
  else:
262
398
  print('Please provide a keyword and file path for grep command')
263
399
 
@@ -266,12 +402,11 @@ def main():
266
402
  if len(args.extra) >= 2:
267
403
  source = args.extra[0]
268
404
  output = args.extra[1]
269
- exclude = args.extra[2:] # Additional arguments as exclude patterns
270
- exclude_params = ' '.join(f'--exclude={x}' for x in exclude)
405
+ exclude = parse_excludes(args.extra[2:]) # Additional arguments as exclude patterns
271
406
  if output.endswith('.tar.gz'):
272
- os.system(f'tar -czvf {output} {exclude_params} {source}')
407
+ run_cmd(['tar', '-czvf', output, *[f'--exclude={x}' for x in exclude], source])
273
408
  elif output.endswith('.tar'):
274
- os.system(f'tar -cvf {output} {exclude_params} {source}')
409
+ run_cmd(['tar', '-cvf', output, *[f'--exclude={x}' for x in exclude], source])
275
410
  else:
276
411
  print('Unsupported output format. Please provide .tar or .tar.gz as the output file extension.')
277
412
  else:
@@ -292,10 +427,15 @@ def main():
292
427
  tar_files = glob.glob(os.path.join(source, '*.tar')) + glob.glob(os.path.join(source, '*.tar.gz'))
293
428
 
294
429
  for tar_file in tar_files:
295
- os.system(f'tar -xzf {tar_file} -C {destination}')
296
- elif source.endswith('.tar.gz') or source.endswith('.tar'):
297
- # Extract a single tar file
298
- os.system(f'tar -xzf {source} -C {destination}')
430
+ if tar_file.endswith('.tar'):
431
+ run_cmd(['tar', '-xvf', tar_file, '-C', destination])
432
+ elif tar_file.endswith('.tar.gz'):
433
+ run_cmd(['tar', '-xzvf', tar_file, '-C', destination])
434
+ # Extract a single tar file
435
+ elif source.endswith('.tar.gz'):
436
+ run_cmd(['tar', '-xzvf', source, '-C', destination])
437
+ elif source.endswith('.tar'):
438
+ run_cmd(['tar', '-xvf', source, '-C', destination])
299
439
  else:
300
440
  print('Please provide a valid .tar or .tar.gz file, or a directory containing such files.')
301
441
 
@@ -303,13 +443,13 @@ def main():
303
443
 
304
444
  elif args.command == 'tar-list':
305
445
  if len(args.extra) > 0:
306
- os.system(f'tar -tvf {args.extra[0]}')
446
+ run_cmd(['tar', '-tvf', args.extra[0]])
307
447
  else:
308
448
  print('Please provide a tar file to list contents')
309
449
 
310
450
  elif args.command == 'tar-add':
311
451
  if len(args.extra) == 2:
312
- os.system(f'tar -rvf {args.extra[1]} {args.extra[0]}')
452
+ run_cmd(['tar', '-rvf', args.extra[1], args.extra[0]])
313
453
  else:
314
454
  print('Please provide a file to add and the target tar file')
315
455
 
@@ -318,17 +458,19 @@ def main():
318
458
  if len(args.extra) == 2:
319
459
  source_dir = args.extra[0]
320
460
  target_dir = args.extra[1]
321
- os.system(f'find {source_dir} -name "*.zip" -exec unzip {{}} -d {target_dir} \;')
461
+ for root, _, files in os.walk(source_dir):
462
+ for name in files:
463
+ if name.endswith('.zip'):
464
+ run_cmd(['unzip', os.path.join(root, name), '-d', target_dir])
322
465
  else:
323
466
  print('Please provide both a source and a target directory.')
324
467
 
325
468
  # Zip compress
326
- elif args.command == 'zip-all' or args.command == 'zip':
469
+ elif args.command == 'zip-all' or args.command == 'zip' or args.command == 'zip-compress':
327
470
  if len(args.extra) >= 2:
328
471
  output = args.extra[0]
329
- sources = args.extra[1:] # All other arguments as sources to be zipped
330
- source_params = ' '.join(sources)
331
- os.system(f'zip -r {output} {source_params}')
472
+ sources = expand_globs(args.extra[1:]) # All other arguments as sources to be zipped
473
+ run_cmd(['zip', '-r', output, *sources])
332
474
  else:
333
475
  print('Please provide an output file name and at least one source to compress.')
334
476
 
@@ -347,10 +489,10 @@ def main():
347
489
  for src_file in source_files:
348
490
  basename = os.path.splitext(os.path.basename(src_file))[0]
349
491
  dest_file = os.path.join(destination_dir, f'{basename}.{file_extension}')
350
- os.system(f'ffmpeg -i "{src_file}" -c copy "{dest_file}" -y')
492
+ run_cmd(['ffmpeg', '-i', src_file, '-c', 'copy', dest_file, '-y'])
351
493
  else:
352
494
  # Handle single file conversion
353
- os.system(f'ffmpeg -i "{source}" -c copy "{destination}" -y')
495
+ run_cmd(['ffmpeg', '-i', source, '-c', 'copy', destination, '-y'])
354
496
  else:
355
497
  print('Usage: convert-video [source file or pattern] [destination file or pattern]')
356
498
 
@@ -1,3 +1,26 @@
1
+ Metadata-Version: 2.4
2
+ Name: linux-command
3
+ Version: 0.3.0
4
+ Summary: A command line tool to perform custom tasks.
5
+ Home-page: https://github.com/MouxiaoHuang/linux-command
6
+ Author: Mouxiao Huang
7
+ Author-email: huangmouxiao@gmail.com
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.6
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Dynamic: author
15
+ Dynamic: author-email
16
+ Dynamic: classifier
17
+ Dynamic: description
18
+ Dynamic: description-content-type
19
+ Dynamic: home-page
20
+ Dynamic: license-file
21
+ Dynamic: requires-python
22
+ Dynamic: summary
23
+
1
24
 
2
25
  # linux-command
3
26
 
@@ -11,6 +34,16 @@ To install the package, run the following command:
11
34
  pip install linux-command
12
35
  ```
13
36
 
37
+ ## Install From Source (Development)
38
+
39
+ If you want to develop or modify the tool locally:
40
+
41
+ ```bash
42
+ git clone https://github.com/MouxiaoHuang/linux-command.git
43
+ cd linux-command
44
+ pip install -e .
45
+ ```
46
+
14
47
  ## Usage
15
48
 
16
49
  Once installed, you can access all commands using `cmd` followed by the specific command name. You can use `cmd -h` or `cmd --help` to see the supported commands. Below is a list of supported commands along with examples for each.
@@ -141,6 +174,12 @@ cmd ps-grep python
141
174
  cmd kill process_name
142
175
  ```
143
176
 
177
+ To force kill:
178
+
179
+ ```bash
180
+ cmd kill --force process_name
181
+ ```
182
+
144
183
  ---
145
184
 
146
185
  ### 21. `df` - Show disk usage in human-readable format
@@ -149,7 +188,7 @@ cmd kill process_name
149
188
  cmd df
150
189
  ```
151
190
 
152
- ### 22. `du [path]` - Show disk usage for a specific file or directory
191
+ ### 22. `du [path]` - Show disk usage for a specific file or directory (default: current directory)
153
192
 
154
193
  ```bash
155
194
  cmd du /path/to/directory
@@ -242,6 +281,21 @@ This command will create a `.zip` file at `/path/to/output.zip` that contains ev
242
281
 
243
282
  ---
244
283
 
284
+ ## Aliases
285
+
286
+ These commands are aliases for the same behavior:
287
+
288
+ - `lsf` = `ls-file`
289
+ - `lsd` = `ls-dir`
290
+ - `ls-bs` = `ls-block-size`
291
+ - `disk` = `du`
292
+ - `tar` = `tar-compress`
293
+ - `untar` = `tar-extract`
294
+ - `zip` = `zip-compress`
295
+ - `zip-all` = `zip-compress`
296
+ - `unzip` = `unzip-all`
297
+ - `convert-vid` = `convert-video`
298
+
245
299
  ## Contributing
246
300
 
247
301
  We welcome contributions from the community! If you'd like to help improve `linux-command`, feel free to report issues or submit pull requests.
@@ -7,4 +7,5 @@ linux_command.egg-info/PKG-INFO
7
7
  linux_command.egg-info/SOURCES.txt
8
8
  linux_command.egg-info/dependency_links.txt
9
9
  linux_command.egg-info/entry_points.txt
10
- linux_command.egg-info/top_level.txt
10
+ linux_command.egg-info/top_level.txt
11
+ tests/test_linux_command.py
@@ -0,0 +1,308 @@
1
+ import builtins
2
+ import signal
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
7
+ sys.path.insert(0, str(PROJECT_ROOT))
8
+
9
+ import linux_command.linux_command as lc
10
+
11
+
12
+ class RunSpy:
13
+ def __init__(self):
14
+ self.calls = []
15
+
16
+ def __call__(self, cmd_args, check=False, **kwargs):
17
+ # Mimic subprocess.run signature used in code.
18
+ self.calls.append(list(cmd_args))
19
+ class Result:
20
+ stdout = ""
21
+ return Result()
22
+
23
+
24
+ class RunCmdSpy:
25
+ def __init__(self):
26
+ self.calls = []
27
+
28
+ def __call__(self, cmd_args):
29
+ self.calls.append(list(cmd_args))
30
+
31
+
32
+ def run_main_with_args(monkeypatch, argv, run_spy=None):
33
+ monkeypatch.setattr(sys, "argv", ["cmd", *argv])
34
+ if run_spy is not None:
35
+ monkeypatch.setattr(lc.subprocess, "run", run_spy)
36
+ lc.main()
37
+
38
+
39
+ def test_ls_and_ls_all(monkeypatch):
40
+ run_spy = RunSpy()
41
+ run_main_with_args(monkeypatch, ["ls"], run_spy)
42
+ run_main_with_args(monkeypatch, ["ls-all"], run_spy)
43
+ assert run_spy.calls[0] == ["ls"]
44
+ assert run_spy.calls[1] == ["ls", "-a"]
45
+
46
+
47
+ def test_ls_variants(monkeypatch):
48
+ run_cmd_spy = RunCmdSpy()
49
+ monkeypatch.setattr(lc, "run_cmd", run_cmd_spy)
50
+ for cmd, expected in [
51
+ ("ls-reverse", ["ls", "-r"]),
52
+ ("ls-time", ["ls", "-lt"]),
53
+ ("ls-long", ["ls", "-l"]),
54
+ ("ls-human", ["ls", "-lh"]),
55
+ ("ls-recursive", ["ls", "-R"]),
56
+ ("ls-recursive-size", ["ls", "-lRh"]),
57
+ ("ls-size", ["ls", "-lS"]),
58
+ ]:
59
+ run_main_with_args(monkeypatch, [cmd])
60
+ assert run_cmd_spy.calls[-1] == expected
61
+
62
+
63
+ def test_ls_block_size_ok(monkeypatch):
64
+ run_cmd_spy = RunCmdSpy()
65
+ monkeypatch.setattr(lc, "run_cmd", run_cmd_spy)
66
+ run_main_with_args(monkeypatch, ["ls-block-size", "M"])
67
+ assert run_cmd_spy.calls[0] == ["ls", "--block-size=M"]
68
+
69
+
70
+ def test_ls_block_size_missing_arg(monkeypatch, capsys):
71
+ run_spy = RunSpy()
72
+ run_main_with_args(monkeypatch, ["ls-block-size"], run_spy)
73
+ out = capsys.readouterr().out
74
+ assert "Please provide a valid block size" in out
75
+
76
+
77
+ def test_ps_user(monkeypatch):
78
+ run_spy = RunSpy()
79
+ run_main_with_args(monkeypatch, ["ps-user", "root"], run_spy)
80
+ assert run_spy.calls[0] == ["ps", "-u", "root"]
81
+
82
+
83
+ def test_ps_variants(monkeypatch):
84
+ run_cmd_spy = RunCmdSpy()
85
+ monkeypatch.setattr(lc, "run_cmd", run_cmd_spy)
86
+ for cmd, expected in [
87
+ ("ps", ["ps"]),
88
+ ("ps-all", ["ps", "-A"]),
89
+ ("ps-aux", ["ps", "aux"]),
90
+ ("ps-sort-memory", ["ps", "aux", "--sort=-%mem"]),
91
+ ("ps-sort-cpu", ["ps", "aux", "--sort=-%cpu"]),
92
+ ]:
93
+ run_main_with_args(monkeypatch, [cmd])
94
+ assert run_cmd_spy.calls[-1] == expected
95
+
96
+
97
+ def test_ps_grep_filters(monkeypatch, capsys):
98
+ def fake_run(cmd_args, check=False, capture_output=False, text=False):
99
+ class Result:
100
+ stdout = "root 1 0.0 0.1 ? Ss 00:00 python\nuser 2 0.0 0.1 ? Ss 00:00 bash\n"
101
+ return Result()
102
+
103
+ monkeypatch.setattr(lc.subprocess, "run", fake_run)
104
+ run_main_with_args(monkeypatch, ["ps-grep", "python"])
105
+ out = capsys.readouterr().out
106
+ assert "python" in out
107
+ assert "bash" not in out
108
+
109
+
110
+ def test_kill_pid(monkeypatch, capsys):
111
+ killed = []
112
+
113
+ def fake_kill(pid, sig):
114
+ killed.append((pid, sig))
115
+
116
+ monkeypatch.setattr(lc.os, "kill", fake_kill)
117
+ run_main_with_args(monkeypatch, ["kill", "1234"])
118
+ assert killed == [(1234, signal.SIGTERM)]
119
+
120
+
121
+ def test_kill_force_pid(monkeypatch):
122
+ killed = []
123
+
124
+ def fake_kill(pid, sig):
125
+ killed.append((pid, sig))
126
+
127
+ monkeypatch.setattr(lc.os, "kill", fake_kill)
128
+ run_main_with_args(monkeypatch, ["kill", "--force", "999"])
129
+ assert killed == [(999, signal.SIGKILL)]
130
+
131
+
132
+ def test_kill_name_confirm(monkeypatch, capsys):
133
+ def fake_run(cmd_args, check=False, capture_output=False, text=False):
134
+ class Result:
135
+ stdout = "root 10 0.0 0.1 ? Ss 00:00 myproc\n"
136
+ return Result()
137
+
138
+ killed = []
139
+
140
+ def fake_kill(pid, sig):
141
+ killed.append((pid, sig))
142
+
143
+ monkeypatch.setattr(lc.subprocess, "run", fake_run)
144
+ monkeypatch.setattr(lc.os, "kill", fake_kill)
145
+ monkeypatch.setattr(builtins, "input", lambda _: "y")
146
+ run_main_with_args(monkeypatch, ["kill", "myproc"])
147
+ assert killed == [(10, signal.SIGTERM)]
148
+
149
+
150
+ def test_du_default(monkeypatch):
151
+ run_spy = RunSpy()
152
+ run_main_with_args(monkeypatch, ["du"], run_spy)
153
+ assert run_spy.calls[0] == ["du", "-sh", "."]
154
+
155
+
156
+ def test_disk_alias(monkeypatch):
157
+ run_cmd_spy = RunCmdSpy()
158
+ monkeypatch.setattr(lc, "run_cmd", run_cmd_spy)
159
+ run_main_with_args(monkeypatch, ["disk", "/tmp"])
160
+ assert run_cmd_spy.calls[0] == ["du", "-sh", "/tmp"]
161
+
162
+
163
+ def test_rm_single_file(monkeypatch, tmp_path):
164
+ target = tmp_path / "a.txt"
165
+ target.write_text("x")
166
+ monkeypatch.setattr(builtins, "input", lambda _: "y")
167
+ run_main_with_args(monkeypatch, ["rm", str(target)])
168
+ assert not target.exists()
169
+
170
+
171
+ def test_rm_pattern(monkeypatch, tmp_path):
172
+ (tmp_path / "a.log").write_text("x")
173
+ (tmp_path / "b.log").write_text("x")
174
+ (tmp_path / "c.txt").write_text("x")
175
+ monkeypatch.setattr(builtins, "input", lambda _: "y")
176
+ run_main_with_args(monkeypatch, ["rm", str(tmp_path), "*.log"])
177
+ assert not (tmp_path / "a.log").exists()
178
+ assert not (tmp_path / "b.log").exists()
179
+ assert (tmp_path / "c.txt").exists()
180
+
181
+
182
+ def test_expand_globs(tmp_path):
183
+ (tmp_path / "x.txt").write_text("x")
184
+ (tmp_path / "y.txt").write_text("x")
185
+ args = [str(tmp_path / "*.txt")]
186
+ expanded = lc.expand_globs(args)
187
+ assert len(expanded) == 2
188
+
189
+
190
+ def test_ls_dir_and_ls_file(monkeypatch, tmp_path, capsys):
191
+ (tmp_path / "a.txt").write_text("x")
192
+ (tmp_path / "b.log").write_text("x")
193
+ (tmp_path / "dir1").mkdir()
194
+ (tmp_path / "dir2").mkdir()
195
+ monkeypatch.chdir(tmp_path)
196
+
197
+ run_main_with_args(monkeypatch, ["ls-dir"])
198
+ out = capsys.readouterr().out.strip()
199
+ assert out == "2"
200
+
201
+ run_main_with_args(monkeypatch, ["ls-file"])
202
+ out = capsys.readouterr().out.strip()
203
+ assert out == "2"
204
+
205
+ run_main_with_args(monkeypatch, ["lsf", "*.log"])
206
+ out = capsys.readouterr().out.strip()
207
+ assert out == "1"
208
+
209
+ run_main_with_args(monkeypatch, ["lsd"])
210
+ out = capsys.readouterr().out.strip()
211
+ assert out == "2"
212
+
213
+
214
+ def test_grep_command(monkeypatch):
215
+ run_cmd_spy = RunCmdSpy()
216
+ monkeypatch.setattr(lc, "run_cmd", run_cmd_spy)
217
+ run_main_with_args(monkeypatch, ["grep", "needle", "file.txt"])
218
+ assert run_cmd_spy.calls[0] == ["grep", "needle", "file.txt"]
219
+
220
+
221
+ def test_tar_compress(monkeypatch):
222
+ run_cmd_spy = RunCmdSpy()
223
+ monkeypatch.setattr(lc, "run_cmd", run_cmd_spy)
224
+ run_main_with_args(monkeypatch, ["tar-compress", "src", "out.tar"])
225
+ run_main_with_args(monkeypatch, ["tar", "src", "out.tar.gz", "--exclude", "node_modules"])
226
+ assert run_cmd_spy.calls[0] == ["tar", "-cvf", "out.tar", "src"]
227
+ assert run_cmd_spy.calls[1] == ["tar", "-czvf", "out.tar.gz", "--exclude=node_modules", "src"]
228
+
229
+
230
+ def test_tar_compress_bad_ext(monkeypatch, capsys):
231
+ run_cmd_spy = RunCmdSpy()
232
+ monkeypatch.setattr(lc, "run_cmd", run_cmd_spy)
233
+ run_main_with_args(monkeypatch, ["tar-compress", "src", "out.zip"])
234
+ out = capsys.readouterr().out
235
+ assert "Unsupported output format" in out
236
+
237
+
238
+ def test_tar_extract_single(monkeypatch):
239
+ run_cmd_spy = RunCmdSpy()
240
+ monkeypatch.setattr(lc, "run_cmd", run_cmd_spy)
241
+ run_main_with_args(monkeypatch, ["tar-extract", "a.tar.gz", "dest"])
242
+ run_main_with_args(monkeypatch, ["untar", "b.tar", "dest"])
243
+ assert run_cmd_spy.calls[0] == ["tar", "-xzvf", "a.tar.gz", "-C", "dest"]
244
+ assert run_cmd_spy.calls[1] == ["tar", "-xvf", "b.tar", "-C", "dest"]
245
+
246
+
247
+ def test_tar_extract_dir(monkeypatch, tmp_path):
248
+ src = tmp_path / "src"
249
+ src.mkdir()
250
+ (src / "a.tar").write_text("x")
251
+ (src / "b.tar.gz").write_text("x")
252
+ run_cmd_spy = RunCmdSpy()
253
+ monkeypatch.setattr(lc, "run_cmd", run_cmd_spy)
254
+ run_main_with_args(monkeypatch, ["tar-extract", str(src), str(tmp_path / "dest"), "all"])
255
+ assert run_cmd_spy.calls[0] == ["tar", "-xvf", str(src / "a.tar"), "-C", str(tmp_path / "dest")]
256
+ assert run_cmd_spy.calls[1] == ["tar", "-xzvf", str(src / "b.tar.gz"), "-C", str(tmp_path / "dest")]
257
+
258
+
259
+ def test_tar_list_and_add(monkeypatch):
260
+ run_cmd_spy = RunCmdSpy()
261
+ monkeypatch.setattr(lc, "run_cmd", run_cmd_spy)
262
+ run_main_with_args(monkeypatch, ["tar-list", "a.tar"])
263
+ run_main_with_args(monkeypatch, ["tar-add", "file.txt", "a.tar"])
264
+ assert run_cmd_spy.calls[0] == ["tar", "-tvf", "a.tar"]
265
+ assert run_cmd_spy.calls[1] == ["tar", "-rvf", "a.tar", "file.txt"]
266
+
267
+
268
+ def test_unzip_all(monkeypatch, tmp_path):
269
+ src = tmp_path / "zips"
270
+ src.mkdir()
271
+ (src / "a.zip").write_text("x")
272
+ (src / "b.txt").write_text("x")
273
+ run_cmd_spy = RunCmdSpy()
274
+ monkeypatch.setattr(lc, "run_cmd", run_cmd_spy)
275
+ run_main_with_args(monkeypatch, ["unzip-all", str(src), str(tmp_path / "out")])
276
+ assert run_cmd_spy.calls[0] == ["unzip", str(src / "a.zip"), "-d", str(tmp_path / "out")]
277
+
278
+
279
+ def test_zip_commands(monkeypatch, tmp_path):
280
+ (tmp_path / "a.txt").write_text("x")
281
+ (tmp_path / "b.txt").write_text("x")
282
+ run_cmd_spy = RunCmdSpy()
283
+ monkeypatch.setattr(lc, "run_cmd", run_cmd_spy)
284
+ run_main_with_args(monkeypatch, ["zip", "out.zip", str(tmp_path / "*.txt")])
285
+ run_main_with_args(monkeypatch, ["zip-compress", "out.zip", str(tmp_path / "a.txt")])
286
+ assert run_cmd_spy.calls[0][0:3] == ["zip", "-r", "out.zip"]
287
+ assert run_cmd_spy.calls[1] == ["zip", "-r", "out.zip", str(tmp_path / "a.txt")]
288
+
289
+
290
+ def test_convert_video_single(monkeypatch):
291
+ run_cmd_spy = RunCmdSpy()
292
+ monkeypatch.setattr(lc, "run_cmd", run_cmd_spy)
293
+ run_main_with_args(monkeypatch, ["convert-video", "a.mp4", "b.mkv"])
294
+ assert run_cmd_spy.calls[0] == ["ffmpeg", "-i", "a.mp4", "-c", "copy", "b.mkv", "-y"]
295
+
296
+
297
+ def test_convert_video_wildcard(monkeypatch, tmp_path):
298
+ src_dir = tmp_path / "src"
299
+ out_dir = tmp_path / "out"
300
+ src_dir.mkdir()
301
+ out_dir.mkdir()
302
+ (src_dir / "a.mp4").write_text("x")
303
+ (src_dir / "b.mp4").write_text("x")
304
+ run_cmd_spy = RunCmdSpy()
305
+ monkeypatch.setattr(lc, "run_cmd", run_cmd_spy)
306
+ run_main_with_args(monkeypatch, ["convert-vid", str(src_dir / "*.mp4"), str(out_dir / "out.mkv")])
307
+ assert run_cmd_spy.calls[0] == ["ffmpeg", "-i", str(src_dir / "a.mp4"), "-c", "copy", str(out_dir / "a.mkv"), "-y"]
308
+ assert run_cmd_spy.calls[1] == ["ffmpeg", "-i", str(src_dir / "b.mp4"), "-c", "copy", str(out_dir / "b.mkv"), "-y"]
File without changes
File without changes
File without changes