-
Notifications
You must be signed in to change notification settings - Fork 0
/
ordermyrepos.py
713 lines (642 loc) · 33.1 KB
/
ordermyrepos.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
#!/usr/bin/python3
from tabulate import tabulate
import argparse
import sys
import shutil
from pathlib import Path
from collections import Counter
import re
import pyperclip
from bs4 import BeautifulSoup
import requests
import subprocess
import shutil
import textwrap
# ANSI escape codes dictionary
colors = {
"BLACK": '\033[30m',
"RED": '\033[31m',
"GREEN": '\033[32m',
"BROWN": '\033[33m',
"BLUE": '\033[34m',
"PURPLE": '\033[35m',
"CYAN": '\033[36m',
"WHITE": '\033[37m',
"GRAY": '\033[1;30m',
"L_RED": '\033[1;31m',
"L_GREEN": '\033[1;32m',
"YELLOW": '\033[1;33m',
"L_BLUE": '\033[1;34m',
"PINK": '\033[1;35m',
"L_CYAN": '\033[1;36m',
"NC": '\033[0m'
}
# Banner printed when commands are displayed
banner: str = r'''________ .___
\_____ \_______ __| _/___________
/ | \_ __ \/ __ |/ __ \_ __ \
/ | \ | \/ /_/ \ ___/| | \/
\_______ /__| \____ |\___ >__|
\/ \/ \/
_____
/ \ ___.__.
/ \ / < | |
/ Y \___ |
\____|__ / ____|
\/\/
__________
\______ \ ____ ______ ____ ______
| _// __ \\____ \ / _ \/ ___/
| | \ ___/| |_> > <_> )___ \
|____|_ /\___ > __/ \____/____ >
\/ \/|__| \/'''
signature = '\n by GunZF0x'
# Define a simple character to print steps
sb: str = f'{colors["L_CYAN"]}[*]{colors["NC"]}'
sb_v2: str = f'{colors["RED"]}[{colors["YELLOW"]}+{colors["RED"]}]{colors["NC"]}'
whitespaces: str = " "*(len('[*]')+1)
warning: str = f'{colors["YELLOW"]}[{colors["RED"]}!{colors["YELLOW"]}]{colors["NC"]}'
# Get commands and arguments from user
def parse_args():
"""
Get commands and flags provided by the user
"""
# Create an ArgumentParser object
#parser = argparse.ArgumentParser(description=print(colors["L_BLUE"] + banner +'\n' + colors["NC"]))
parser = argparse.ArgumentParser()
# Define commands
subparsers = parser.add_subparsers(dest='command')
# Command 1: add -- Add function to 'repo file'
add_command: str = 'add'
command1_parser = subparsers.add_parser(add_command, help='Add Github repositories to a "repo file" used to list and order your repositories')
command1_parser.add_argument("-w", "--webpage", type=str, required=True, metavar='https://github.com/user/example',
help="Webpage to extract the content. For example, a Github repository url (without .git at the end of it)")
command1_parser.add_argument("-c", "--clone", action="store_true",
help="Use this flag if you want to, additionally, clone the repository on your machine.")
command1_parser.add_argument("-f", "--filename", type=str, default="repositories.txt", metavar='filename',
help="File to write the content extracted. If not specified, it will be saved in './repositories.txt' as default")
command1_parser.add_argument("--print-only", action="store_true",
help = "Only Print Mode. Use this flag if you do not want to add the description obtained to a file, i.e., only prints output")
command1_parser.add_argument("-t", "--title", type=str, metavar = '"string title"',
help="Custom title for the repository. If not set, Github's default title will be used")
command1_parser.add_argument("-l", "--language", type=str, help="Custom language for the repository. If not set, it will use the language with a higher percentage in Github's language bar")
command1_parser.add_argument("-os", "--operating-system", type=str, metavar = '{W, L, A}',
help="Operating system scope for the repository. Options: '(W)indows', '(L)inux' or '(A)ny' (default: 'Any')")
command1_parser.add_argument("-x", "--html-class", type=str, default="f4 my-3",
help="HTML class containing the description for the repository (default in Github: 'f4 my-3')")
# Command 2: show -- Show the contents of 'repo file'
show_command = 'show'
command2_parser = subparsers.add_parser(show_command, help='Display repositories contained in the "repo file" in a pretty table format')
command2_parser.add_argument("-f", "--filename", type=str, default = "repositories.txt", metavar = 'filename',
help="Filename containing repositories to display; usually the file generated by addRepo.py. Default: ./repositories.txt")
command2_parser.add_argument("-s", "--search", type=str, help="Search for a word/string into Repository Name or its Description", metavar='string')
command2_parser.add_argument("-l", "--only-language", help="Search for a specific language. Case insensitive.", metavar='language')
command2_parser.add_argument("-x", "--only-os", type=str, help="Search for a specific repository whose scope is the one in this flag. Valid values: (A)ny, (L)inux, (W)indows",
metavar='OS')
command2_parser.add_argument("-c", "--copy", action="store_true", help="Copy the resulting repositories, after filtering, into clipboard")
command2_parser.add_argument("-o", "--output", type=str, help="Output filename to save the table", metavar='filename')
command2_parser.add_argument("--first", type=int, help="Select the first N results/rows", metavar='N')
command2_parser.add_argument("--last", type=int, help="Select the last N results/rows", metavar='N')
command2_parser.add_argument("-tf", "--table-format", type=str, default="grid", metavar = 'table_format',
help="Table format output. Check 'Table format' section at https://pypi.org/project/tabulate/ to see all options available. Some of them might be bugged. For some formats it is necessary to additionally use '--no-color' flag")
command2_parser.add_argument("--no-author", action="store_true", help="Do not show author name in repository name/only show repository name")
command2_parser.add_argument("--sort-by-author", action="store_true", help="Sort repositories alphabetically based on repos' authors")
command2_parser.add_argument("--sort-by-repo", action="store_true",
help="Sort repositories alphabetically by their name (not considering authors)")
command2_parser.add_argument("--sort-by-language", action="store_true",
help="Sort repositories alphabetically by their main programming language")
command2_parser.add_argument("--show-stats", action="store_true",
help="Display some statistics about the repositories (such as number of languages, distribution by OS, etc)")
command2_parser.add_argument("--no-color", action="store_true", help="Do not display the table with colors")
# Parse the command-line arguments
args = parser.parse_args()
return parser, args
###############
# Add command #
###############
def remove_numbers(string):
"""
Remove all words that are exclusively a number/float from a string.
Example, "go1 123.5 aaaa j2 400" will be filtered as "go1 aaaa j2"
"""
return re.sub(r'\b\d+(?:\.\d+)?\b', '', string).strip().replace(' ', ' ')
def check_HTTP_status_code(args_var, OS):
"""
Check HTTP status code for a web request. If it exists return its content, else executes an error
"""
# Send an HTTP request to the URL requested by the user
print(f"{sb} Sending HTTP request to {args_var.webpage}...")
r = requests.get(args_var.webpage)
# Check HHTP status code. If the page does not responds, exit and print the HTTP status code
if r.status_code != 200:
print("Error: HTTP status code {r.status_code}")
sys.exit(1)
# Get the HTML content of the webpage
soup = BeautifulSoup(r.text, "lxml")
# Find all the div elements with the class "item"
items = soup.find_all(class_=args_var.html_class)
# Find main header/title if it was set by the user, otherwise use Github main header
if args_var.title:
header = args_var.title
else:
header = soup.title.text
header = header.replace('GitHub - ', '')
header = header.split()[0]
header = header[:-1]
# Find programming language if it was set by the user, otherwise use Github language bar and take the one with higher percentage
if not args_var.language:
language = soup.find("span", attrs={'class': 'Progress-item color-bg-success-emphasis'})
try:
language = language.get("aria-label")
language = remove_numbers(language)
except AttributeError:
print(f"{warning} {colors['RED']}Warning!{colors['NC']} No programming language found for the repository. Using 'None' as default")
language = 'None'
else:
language = args_var.language
# Check how many items have been return and, in function of that, display a certain message
if len(items) > 1:
print(f"{sb}Warning! More than 1 items found. Found {len(items)} items.")
print("{sb}This script will only return the first item found. However, ", end='')
print("all the items found are:")
if len(items) == 1:
print(f"{sb} Description found!\n")
if len(items) == 0:
print(f'{warning} No description found')
print(f'\n{whitespaces}{sb_v2}{colors["CYAN"]} Title: {colors["NC"]}{colors["L_GREEN"]}"{header}"{colors["NC"]}')
print(f'{whitespaces}{sb_v2}{colors["CYAN"]} Operating System: {colors["NC"]}{colors["L_GREEN"]}{OS}{colors["NC"]}')
print(f'{whitespaces}{sb_v2}{colors["CYAN"]} Language: {colors["NC"]}{colors["L_GREEN"]}{language}{colors["NC"]}')
print(f"{whitespaces}{sb_v2}{colors['CYAN']} Description: {colors['NC']}NO DESCRIPTION")
print()
return header, language, 'NO DESCRIPTION'
# Create a simple array that will store the description string
fixed_description = []
# Print the webpage/repository title
print(f'{whitespaces}{sb_v2}{colors["CYAN"]} Title: {colors["NC"]}{colors["L_GREEN"]}{header}{colors["NC"]}')
# Print the OS provided by the user
print(f'{whitespaces}{sb_v2}{colors["CYAN"]} Operating System: {colors["NC"]}{colors["L_GREEN"]}{OS}{colors["NC"]}')
# Print the programming language provided by the user/detected by BeautifulSoup
print(f'{whitespaces}{sb_v2}{colors["CYAN"]} Language: {colors["NC"]}{colors["L_GREEN"]}{language}{colors["NC"]}')
# Print the text of each items
for i, item in enumerate(items):
# Each description may have spaces before and after words, so filter them with split function and rebuild the description
word_list = item.text.split()
description: str = ''
for j, word in enumerate(word_list):
if j != len(word_list)-1:
description += f"{word} "
else:
description += word
there_is_offset = False
# Check if the description has a length that fits with the width of the current terminal
one_terminal_line_width = shutil.get_terminal_size()[0]
if (len(description) + len(whitespaces) + len(sb) + len("Description:") + 4) >= one_terminal_line_width:
offset = '\n' + ' ' * (len(' [+] Description:') + 2)
maxlength_available = one_terminal_line_width - len(whitespaces) - len(sb) - len("Description:") - 3
there_is_offset = True
# Create an alternative description only for print. Original description returned will not be wrapped
description_alternative = offset.join(textwrap.wrap(description,width=maxlength_available))
if len(items) == 1:
print(f'{whitespaces}{sb_v2}{colors["CYAN"]} Description: {colors["NC"]}', end='')
if there_is_offset:
print(f'{colors["L_GREEN"]}"{description_alternative}"{colors["NC"]}')
else:
print(f'{colors["L_GREEN"]}"{description}"{colors["NC"]}')
fixed_description.append(description)
break
else:
print(f'{whitespaces}{colors["RED"]}{i+1}) "{description}"{colors["NC"]}')
fixed_description.append(description)
print("")
return header, language, fixed_description[0]
def check_if_print_only_mode_is_enabled(args_var) -> None:
"""
Check is the user wants to write the output into a file or only wants to print the info/output
"""
if args_var.print_only:
print(f'{sb} "Print Only" Mode enabled. No description added to file.')
sys.exit(0)
return
def ask_to_user_if_wants_to_write(name_file: str) -> None:
"""
Asks to the user if wants to write the output into a file
"""
ask_user = input(f"{whitespaces}Would you like to write the output to '{name_file}' file? {colors['YELLOW']}[Y (or Enter)/N]{colors['NC']}: ")
print()
# If the user just presses Enter
if not ask_user:
return
# If the user
if re.match(r"^y(es)?$", ask_user, re.IGNORECASE):
return
elif re.match(r"^n(o)?$", ask_user, re.IGNORECASE):
print(f"{whitespaces}{sb_v2} Data will not be saved into a file. Exiting...")
sys.exit(1)
else:
print(f"{whitespaces}{sb_v2} Invalid option. Exiting...")
sys.exit(1)
def check_operating_system(args_var) -> str:
"""
Check operating system provided by the user. If not provided or input is invalid, returns 'Any' as default
"""
# Check if the user has provided a flag for 'Operating System'
if not args_var.operating_system:
print(f'{warning} {colors["RED"]}Warning!{colors["NC"]} No Operating System selected. Selecting "Any" as Operating System (default)')
return "Any"
# If the user has provided it, select if it is for Windows, Linux, Any or is incorrect
if re.match(r"^w(indows)?$", args_var.operating_system, re.IGNORECASE): # re.IGNORECASE is case-insensitive, i.e., 'windows = WindoWS'
return "Windows"
elif re.match(r"^l(inux)?$", args_var.operating_system, re.IGNORECASE):
return "Linux"
elif re.match(r"^a(ny)?$", args_var.operating_system, re.IGNORECASE):
return "Any"
else:
print(f'{warning} Warning! Invalid Operating System provided ("{args_var.operating_system}"). Returning "Any" as Operating System (default)')
return "Any"
def check_file_to_write(args_var, description: str, header: str, OS_selected: str, language: str) -> None:
"""
Checks if the file to write the output exists. If it exists, add a line. If not, creates it.
"""
file_to_append = Path.cwd().joinpath(args_var.filename)
# The following string will be added to the file given by the user
description_to_add = f"{args_var.webpage}.git -- {header} -- {OS_selected} -- {language} -- {description}\n"
# Get path to the file
file_path = Path(file_to_append)
# If the file provided by the user does not exists, create a new one
if not file_path.exists():
print(f"{sb} {colors['RED']}Warning{colors['NC']}: '{file_to_append}' does not exist. ", end='')
create_file = input(f"Would you like to create '{file_path.name}' file? {colors['YELLOW']}[Y/N]{colors['NC']}: ")
if re.match(r"^y(es)?$", create_file, re.IGNORECASE): # re.IGNORECASE is case-insensitive, i.e., 'yes = YeS'
with open(file_to_append, 'x') as f: # 'x' will raise an error if the file already exists
f.write(description_to_add)
print(f"{sb} Description added to the recently created file '{file_to_append}'")
return
elif re.match(r"^n(o)?$", create_file, re.IGNORECASE):
print(f"{sb} No file will be created. Exiting...")
sys.exit(1)
else:
print(f"{sb} Error: No valid argument provided. Exiting...")
sys.exit(1)
if file_path.exists():
# Check if the repository has not been already/previously added
with open(file_to_append, 'r') as f:
lines = f.readlines()
# Check if the first column is already present
is_repo_already_added = any(line.startswith(args_var.webpage) for line in lines)
# If the repository has not been previously added, add it
if not is_repo_already_added:
with open(file_to_append, 'a') as f:
f.write(description_to_add)
print(f"{sb} Description added to file!")
# If the repository has been previously added finish the program with an error
if is_repo_already_added:
print(f"{sb} {args_var.webpage} has already been added. Exiting...")
sys.exit(1)
return
def clone_repo(args_var) -> None:
"""
Clone the repository if the user passed the flag '--clone' or '-c'
"""
if args_var.clone:
print(f"{sb} Cloning {args_var.webpage}.git...")
clone_command = subprocess.run(["git", "clone", f"{args_var.webpage}.git"])
if clone_command.returncode == 0:
print(f"{sb} Repository cloned succesfully!")
else:
print(f"{sb} {colors['RED']}Warning:{colors['NC']}Failed when trying to clone the repository...")
return
################
# show command #
################
def print_colors(no_color_boolean: bool,
message: str, mode='output') -> None:
"""
Simple message handler for '--no-color' flag
"""
if mode == "output":
if no_color_boolean:
print(f"[*] {message}")
else:
print(f"{sb} {message}")
return
if mode == "warning":
if no_color_boolean:
print(f"[!] Warning! {message}")
else:
print(f"{warning} {colors['RED']}Warning!{colors['NC']} {message}")
return
if mode == "error":
if no_color_boolean:
print(f"[!] Error: {message}")
else:
print(f"{warning} {colors['RED']}Error: {message}{colors['NC']}")
return
print("No valid mode selected")
sys.exit(1)
def get_percentage(number: int, total: int) -> float:
"""
Returns the percentage a number represents within a total
"""
return round(number/(1.0*total) * 100, 1)
def stats_table_elements(flags_var, table_elements):
"""
Analyze some elements extracted from the repositories file
"""
# Check the number of programming languages
languages = [item[3].lower() for item in table_elements]
counter_object = Counter(languages)
keys = counter_object.keys()
num_values = len(keys)
# Get OS details
OS_list = [item[2] for item in table_elements]
counts_OS = Counter(OS_list)
any_percentage = get_percentage(counts_OS['Any'], len(OS_list))
linux_percentage = get_percentage(counts_OS['Linux'], len(OS_list))
windows_percentage = get_percentage(counts_OS['Windows'], len(OS_list))
if flags_var.no_color:
print(f"[*] Number of different languages: {num_values}")
print("[*] Operating System scope:")
print(f"{whitespaces}[+] Any: {counts_OS['Any']} ({any_percentage}%)")
print(f"{whitespaces}[+] Linux: {counts_OS['Linux']} ({linux_percentage}%)")
print(f"{whitespaces}[+] Windows: {counts_OS['Windows']} ({windows_percentage}%)")
else:
print(f"{sb} Number of different languages: {colors['PURPLE']}{num_values}{colors['NC']}")
print(f"{sb} Operating System scope:")
print(f"{whitespaces}{sb_v2} Any: {colors['PURPLE']}{counts_OS['Any']}{colors['NC']} ({colors['RED']}{any_percentage}%{colors['NC']})")
print(f"{whitespaces}{sb_v2} Linux: {colors['PURPLE']}{counts_OS['Linux']}{colors['NC']} ({colors['RED']}{linux_percentage}%{colors['NC']})")
print(f"{whitespaces}{sb_v2} Windows: {colors['PURPLE']}{counts_OS['Windows']}{colors['NC']} ({colors['RED']}{windows_percentage}%{colors['NC']})")
return
def delete_git_extension(no_color_boolean: bool, url: str):
"""
Delete ".git" extension from weblink repository column
"""
# If the last 4 characters are ".git"
if url[-4:] == '.git':
# Return url without ".git" extension
return url[:-4]
else:
print_colors(no_color_boolean, "Unable to cut '.git' string in '{url}'. Returning original url instead...",
mode='warning')
return url
def filter_data_table(flags_var, printable_data_table):
"""
Function that sorts and filters table body/data based on flags provided by the user
"""
# Save the original table length for analytics purposes
original_length_table = len(printable_data_table)
# Sort repositories alphabetically by their author
if flags_var.sort_by_author:
temp_data_table = sorted(printable_data_table, key=lambda x: x[1])
printable_data_table = temp_data_table
# Sort repositories aplhabetically by its programming language
if flags_var.sort_by_language:
if flags_var.only_language:
print_colors(flags_var.no_color, "'--sort-by-language' and '--only-language' flags simultaneously enabled...",
mode='warning')
temp_data_table = sorted(printable_data_table, key= lambda x: x[3])
printable_data_table = temp_data_table
# Sort repositories alphabetically by repository name
if flags_var.sort_by_repo:
if flags_var.sort_by_author or flags_var.sort_by_repo:
print_colors(flags_var.no_color, "Multiple 'sort' flags simultaneously enabled. Output will be sortered by repository name...",
mode='warning')
try:
temp_data_table = sorted(printable_data_table, key=lambda x: x[1].split('/')[1])
printable_data_table = temp_data_table
except:
print_colors(flag_var.no_color, "Unable to sort by author", mode='warning')
# Filter by programming language
if flags_var.only_language:
temp_data_table = []
for rows in printable_data_table:
if rows[3].lower() == flags_var.only_language.lower():
temp_data_table.append(rows)
if len(temp_data_table) == 0:
print_colors(flags_var.no_color, f"No results found for language '{flags_var.only_language}'...",
mode='error')
sys.exit(1)
printable_data_table = temp_data_table
# Filter by searching a word in the 'Description' column
if flags_var.search:
temp_data_table = []
for rows in printable_data_table:
for n_col, cols in enumerate(rows):
# Check for a string, except in Github weblink, OS or Programming language
if n_col in [0,2,3]:
continue
if flags_var.search.lower() in cols.lower():
temp_data_table.append(rows)
break
if len(temp_data_table) == 0:
print_colors(flags_var.no_color, f"Word '{flags_var.search}' could not be found for any repository...",
mode='error')
sys.exit(1)
printable_data_table = temp_data_table
# Filter by Operating System
if flags_var.only_os:
if re.match(r"^w(indows)?$", flags_var.only_os, re.IGNORECASE):
only_os_var = "Windows"
elif re.match(r"^l(inux)?$", flags_var.only_os, re.IGNORECASE):
only_os_var = "Linux"
elif re.match(r"^a(ny)?$", flags_var.only_os, re.IGNORECASE):
only_os_var = "Any"
else:
print_colors(flags_var.no_color, f"'{flags_var.only_os}' is not a valid value for '--only-os' flag. Valid values: (A)ny, (L)inux, (W)indows. Please retry...",
mode='warning')
sys.exit(1)
temp_data_table = []
for rows in printable_data_table:
if rows[2] == only_os_var and only_os_var != '':
temp_data_table.append(rows)
if len(temp_data_table) == 0:
print_colors(flags_var.no_color, f"No items found for {only_os_var} OS...",
mode = 'error')
sys.exit(1)
printable_data_table = temp_data_table
# Select the first N rows if '--first' flag is provided
if flags_var.first:
if flags_var.first > len(printable_data_table):
print_colors(flags_var.no_color, f"Unable to show first {flags_var.first} rows since max number of rows is {len(printable_data_table)}. Displaying full table...",
mode='warning')
else:
printable_data_table = printable_data_table[:flags_var.first]
# Select the last N rows if '--last' flag is enabled
if flags_var.last:
if flags_var.first:
print_colors(flags_var.no_color, f"'--first' and '--last' simultaneously enabled. Result will be cut only considering '--first' flag (first {flags_var.first} rows)...",
mode='warning')
if not flags_var.first and flags_var.last > len(printable_data_table):
print_colors(flags_var.no_color, f"Unable to show last {flags_var.last} rows since max number of rows is {len(printable_data_table)}. Displaying full table...",
mode='warning')
else:
printable_data_table = printable_data_table[(-1 * flags_var.last):]
# Only show repository name, no author
if flags_var.no_author:
for index, row in enumerate(printable_data_table):
try:
printable_data_table[index][1] = row[1].split('/')[1]
except:
pass
# Copy lists, after filtering, to output
if flags_var.copy:
print()
if len(printable_data_table) == 0:
print_colors(flags_var.no_color, "Repository list is empty", mode='error')
sys.exit(1)
if len(printable_data_table) == 1:
print_colors(flags_var.no_color, "Repository copied to clipboard!",
mode='output')
pyperclip.copy(delete_git_extension(flags_var.no_color, printable_data_table[0][0]))
if len(printable_data_table) > 1:
repos = []
for row in printable_data_table:
repos.append(delete_git_extension(flags_var.no_color, row[0]))
repos_string = '\n'.join(map(str, repos))
pyperclip.copy(repos_string)
print_colors(flags_var.no_color, "Multiple repositories copied to clipboard!",
mode='output')
# Shows statistics
if flags_var.show_stats:
if len(printable_data_table) < original_length_table:
if flags_var.no_color:
print(f"[*] Number of repositories after applying filters: {len(printable_data_table)}")
else:
print(f"{sb} Number of repositories after applying filters: {colors['PURPLE']}{len(printable_data_table)}{colors['NC']}")
# Show some statistics for the data that will be returned and shown in the final table
stats_table_elements(flags_var, printable_data_table)
return printable_data_table
def create_table_elements(flags_var, width_terminal, printable_data_rows_table):
"""
Add colors to the table and sets their parts ready to be printed
"""
# Headers for the table
headers_table = ["Repository Name", "OS", "Language", "Description"]
# Get the max length (the sum of them) for columns that are not the "Description column"
max_length = 0
table_to_show = [[item for item in sublist[1:]] for sublist in printable_data_rows_table]
for col in table_to_show:
new_length = len(col[0]) + len(col[1]) + len(col[2]) + 12 # '12' considering symbols and spaces
if new_length > max_length:
max_length = new_length
# Max allowed length before 'wrapping' text
max_allowed_length = width_terminal - max_length - 12
if flags_var.no_color:
return headers_table, table_to_show, max_allowed_length
else:
colors_headers_table = [f"{colors['L_CYAN']}Repo Name{colors['NC']}",
f"{colors['PINK']}OS{colors['NC']}",
f"{colors['L_RED']}Language{colors['NC']}",
f"{colors['L_GREEN']}Description{colors['NC']}"]
# Create a table body containing ANSI escape codes so it will print in colors
colors_row_table = []
for row in table_to_show:
color_column = []
# 'Repo Name' column
color_column.append(f"{colors['CYAN']}{row[0]}{colors['NC']}")
# 'Operating System (OS)' column
color_column.append(f"{colors['PURPLE']}{row[1]}{colors['NC']}")
# 'Language' column
color_column.append(f"{colors['RED']}{row[2]}{colors['NC']}")
# 'Description' column
color_column.append(f"{colors['GREEN']}{row[3]}{colors['NC']}")
colors_row_table.append(color_column)
return colors_headers_table, colors_row_table, max_allowed_length
def read_columns_in_repository_file(flag_var):
"""
Read the repositories file where the separator is "--"
"""
rows = []
with open(flag_var.filename, "r") as file:
for line in file:
col = []
row = line.strip().split("--")
for column in row:
col.append(column.strip())
rows.append(col)
if not flag_var.show_stats:
return rows
print()
if flag_var.no_color:
print(f"[*] Total number of repositories: {len(rows)}")
else:
print(f"{sb} Total number of repositories: {colors['PURPLE']}{len(rows)}{colors['NC']}")
return rows
def check_file_to_read(flags_var) -> None:
"""
Check if the file that stores all the repositories exists
"""
# Get the path where the script is being executed (current path, not where the script is stored)
file_to_read = Path.cwd().joinpath(flags_var.filename)
# Get file path
file_path = Path(file_to_read)
# Check if the file containing the repositories exists
if not file_path.exists():
print_colors(flags_var.no_color, f"{file_to_read}' does not exist. Try using 'ordermyrepos.py add' command to create a file and retry",
mode='error')
sys.exit(1)
return None
def print_table(flags_var, body_table, headers_table, max_allowed_length):
"""
Print the final table/result
"""
print()
print(tabulate(body_table,
headers=headers_table, tablefmt=flags_var.table_format,
maxcolwidths=[None, None, None, max_allowed_length]))
def save_file(flags_var, body_table, max_allowed_length):
"""
Save the table displayed in a 'no color' format to avoid ANSII escape characters
"""
if not flags_var.output:
return
headers_table = ["Repository Name", "OS", "Language", "Description"]
table_to_save = tabulate(body_table,
headers=headers_table, tablefmt=flags_var.table_format,
maxcolwidths=[None, None, None, max_allowed_length])
with open(flags_var.output, "w") as file:
file.write(table_to_save)
return
# MAIN
def main() -> None:
# Parse the command-line arguments
parser, args = parse_args()
# Call 'add' command/function
if args.command == 'add':
# Check Operating System scope provided by the user (default: Any)
OS = check_operating_system(args_var=args)
# Get the description/items from the webpage
header_obtained, language_obtained, description_obtained = check_HTTP_status_code(args, OS)
# Check if the user wants to write output on a file (default: True)
check_if_print_only_mode_is_enabled(args_var=args)
# Ask to the user if wants to write the output shown above to the file
ask_to_user_if_wants_to_write(args.filename)
# Check if the file to write output exists
check_file_to_write(args_var=args, description=description_obtained,
header=header_obtained, OS_selected=OS, language=language_obtained)
# Check if the user wants to clone the repository in the current directory
clone_repo(args_var=args)
# Done
print(f"{sb} Done!")
# Call 'show' command/function
elif args.command == 'show':
# Check if the file that contains the repositories data exists (if not, exits)
check_file_to_read(args)
# Get terminal width
width = shutil.get_terminal_size()[0]
# Get data contained in the file
printable_data_table = read_columns_in_repository_file(args)
# Sort/filter the list (or not) based on flags provided
filtered_printable_data_table = filter_data_table(args, printable_data_table)
# Create table body that will be printed
headers_table, body_table, max_allowed_length = create_table_elements(args, width, filtered_printable_data_table)
# Print the table
print_table(args, body_table, headers_table, max_allowed_length)
# Save the file (if requested by the user)
save_file(args, filtered_printable_data_table, max_allowed_length)
# If user provides no argument
else:
print(colors["L_BLUE"] + banner +'\n' + colors["RED"] + signature + '\n' + colors["NC"])
parser.print_help() # Display help message for invalid command
return None
if __name__ == "__main__":
main()