Использование Linux или Unix-подобных операционных систем дает вам множество отличных инструментов командной строки, некоторые из них вы будете использовать несколько раз в день, а некоторые, возможно, никогда не будете использовать.
Ради обучения и получения лучше разбираясь в Python и C, я решил начать воссоздавать уже существующие команды Linux и некоторые инструменты кибербезопасности.
Сегодня мы обсудим то, что я узнал, воссоздав одну из наиболее часто используемых команд Linux, Cat.

Делаем это сначала на Python

Идея начать с Python проста. Я хочу иметь представление о том, что делать и чего ожидать при переходе на C. Это поможет мне сократить время завершения проекта.
Итак, в этой статье будет версия Python, а в следующей статье мы обсудим версию C.

Одно можно сказать наверняка, это самая легкая часть всего этого.

Я начал с написания простой документации и списка дел:

"""
*******************************************************************
** This is a re-creation of the OG Linux command "cat".          **
** This project is for educational purposes only.                **
** No program is free from bugs, so run it at your own risk.     **
** This is the Python version, a C version also be shared later. **
*******************************************************************
Made by shelldawg.
-> github.com/shelldawg


TODO:
    [ ] - Check file exist or not
    [ ] - Basic read in case file exists
    [ ] - separate options from files
    [ ] - Work on the options
"""

Это очень хорошая привычка, к которой можно привыкнуть при написании кода. Начните с определения ваших шагов и разбивки проблемы на более мелкие части, это поможет вам сделать все быстро, поскольку вы уже знаете, что делать.

Я разбил программу на 4 шага:
1. Проверить, существует ли файл
2. Базовое чтение файла, если файл существует
3. Отделить параметры от файлов
4. Работать по вариантам

Шаги 1 и 2 обеспечивают базовую функциональность.
Затем мы переходим к обогащению программы с точки зрения функциональности.

Шаг 1 и 2

import sys
import os

args = sys.argv[1::] # this to skip the script's name
file = args[0]

def getPath(file):
    return os.getcwd() + '/' + file

# here we expect that theres on arg and its the file name
if os.path.isfile(getPath(file)):
 with open(getPath(file), 'r') as text_file:
     f = text_file.readlines()

 print("".join(f))

Этого фрагмента кода достаточно для решения первых двух задач.

Краткое объяснение: мы импортируем sys и os, а затем получаем встроенные аргументы с sys.argv[1::], здесь я использовал [1::], чтобы пропустить элемент в индексе 0, потому что это будет имя скрипта.
Затем мы берем первый элемент из args[0] и помещаем его в переменную с именем file, это делается только для того, чтобы избежать повторения args[0]. Кроме того, поскольку это простая демонстрация с базовыми функциями, мы ожидаем, что пользователь введет один аргумент, и это будет файл для чтения.
Затем мы создали простую функцию getPath(файл); его роль состоит в том, чтобы взять имя файла в качестве параметра, а затем вернуть os.getcwd() + '/' + файл. Это делает две вещи:
1. >os.getcwd() возвращает текущий рабочий каталог (например, /home/user/lab)
2. затем мы объединяем его с '/' и имя файла
Окончательный путь будет выглядеть так: /home/user/lab/file.txt

Затем мы вводим оператор if, мы использовали os.path.isfile(), он принимает путь к файлу (в этом случае мы использовали нашу собственную функцию для создания полный путь к файлу), если файл существует, он вернет True; в противном случае False.
Если true, то мы продолжаем читать содержимое файла построчно. Это облегчит нам реализацию некоторых опций. Подробнее об этом позже.

Теперь, когда у нас есть очень простая программа, мы можем добавить больше функций.

Шаг 3:

Перед разделением аргументов и имен файлов я протестировал настоящую команду cat в своем терминале, оказалось, если вы указали недопустимую опцию, она вообще не запустится, а если все опции действительны (или вообще отсутствуют), она будет распечатать содержимое файла/ов, если файл существует, в противном случае он сообщает вам, что файла нет.
На основе этой информации я написал этот код для обработки шага № 3:

validOptions= ["-b", "--number-nonblank", "-E", "--show-ends", "-n", 
               "--number", "-s", "--squeeze-blank", "-t", "-T", 
               "--show-tabs", "--help"]

options = [] # this will hold options found
files   = [] # this will hold files found

for arg in args: # separationg options from files
    if '-' in arg[0]:
        if validOptions.count(arg):
            options.append(arg)
        else:
            print(f"dawgcat: invalid option: {arg}\nTry 'python dawgcat.py --help' for more information.")
            exit()
    elif (os.path.isfile(getPath(arg))): 
        files.append(arg)
    else:
        print(f"dawgcat: {arg}: : No such file or directory")

Здесь у нас есть простой цикл for, который перебирает переменную args, которую мы создали на шагах 1 и 2.

Первый оператор if должен определить, начинается ли аргумент с «-», так как все наши опции будут начинаться с него (например: — help, -b…)

Это важно, потому что в именах файлов может быть «-». Так что не стоит просто проверять, существует ли «-» в аргументе.

Если это так, мы снова проверяем, существует ли этот аргумент внутри массива validOptions, и если да, то добавляем его в options, который содержит найденные допустимые параметры.
Если его нет в допустимом списке, мы сообщаем пользователю, что параметр недействителен, и мы exit() программа сразу.

Если аргумент не начинается с дефиса '-', мы проверяем, существует ли файл, как мы это делали в шагах 1 и 2. Если да, мы добавляем его в массив files. .
В противном случае мы просто сообщаем пользователю, что файл не существует, и переходим к следующей итерации цикла.

И вот как я справился с этой проблемой. Если у вас есть идея получше, дайте мне знать в комментариях ниже!
Теперь перейдем к финальному бою с боссом, чтобы варианты заработали.

Наконец, Шаг №4!

Здесь я начал с простой функции помощи.

class bcolors:
# this is taken from stackoverflow.com/questions/287871/how-do-i-print-colored-text-to-the-terminal
    HEADER = '\033[95m'    
    OKBLUE = '\033[94m'
    OKCYAN = '\033[96m'
    OKGREEN = '\033[92m'
    WARNING = '\033[93m'
    FAIL = '\033[91m'
    ENDC = '\033[0m'
    BOLD = '\033[1m'
    UNDERLINE = '\033[4m'

def printHelp():
    print(bcolors.OKGREEN,
"""    ____                                  __ 
   / __ \____ __      ______ __________ _/ /_
  / / / / __ `/ | /| / / __ `/ ___/ __ `/ __/
 / /_/ / /_/ /| |/ |/ / /_/ / /__/ /_/ / /_  
/_____/\__,_/ |__/|__/\__, /\___/\__,_/\__/  
                     /____/                   
                     """,bcolors.ENDC)
    print("Usage: python dawgcat.py [OPTIONS] [FILE]")
    print("Concatenate FILE(s) to standard output.")
    print("""
  -b, --number-nonblank    number nonempty output lines, overrides -n 
  -E, --show-ends          display $ at end of each lines
  -n, --number             number all output lines
  -s, --squeeze-blank      suppress repeated empty output lines
  -T, --show-tabs          display TAB characters as ^I
      --help        display this help and exist
""")
    print(bcolors.HEADER,"Help menu is literally not from OG cat command.",bcolors.ENDC)
    exit()

После вызова printHel() он распечатает всю эту информацию и закроет программу. Поскольку почти все команды делают это, я сделал то же самое.

if("--help" in " ".join(args).lower()) or len(args) == 0:
    printHelp() # the function has exit() so the program stops here

Я также добавил эту проверку, чтобы увидеть, был ли вызов опции — help или нет аргументов.
Я также использовал фрагмент кода из stackoverflow, чтобы придать меню справки еще немного жизни. Кредиты идут на stackoverflow.com/users/19104/joeld.

Теперь варианты:

def applyOptions(file, length = 0):
    # this returns the file as it is if there are no options
    if len(options) == 0:
        return file

    #removing overwriten option (e.g: if -b and -n exists only -n stays)
    if "-n" in options and "-b" in options:
        options.remove("-n")
    
    for option in options:
        if option == "-s" or option == "--squeeze-blank":
            file = [line for line in file if line.strip() != '']
        if option == "-E" or option == "--show-ends": 
            file = [line.replace("\n", "$\n") for line in file]
        if option == "-T" or option == "--show-tabs":
            file = [line.replace("\t", "^I") for line in file]
        if option == "-n" or option == "--number":
            file = ["{:>6}  {}".format(i+1+length, file[i]) for i in range(len(file))]
        if option == "-b" or option == "--number-nonblank":
            # file = ["{:>6}  {}".format(counter+length, file[i]) if file[i].strip() != '\n' else file[i] for i in range(len(file)) ]
            # I tried to do it in line but it failed so many times. I guess this is the better way now.
            counter = 1 
            for i in range(len(file)):
                if file[i].strip() != "":
                    file[i] = "{:>6}  {}".format(counter+length, file[i])
                    counter += 1
                else: file[i]=file[i] 
    return file

Этот фрагмент кода перебирает каждый элемент массива options, который мы создали ранее.
Он принимает два аргумента, один из которых является обязательным, а другой — необязательным.
Это файл и длина. File для изменения и возврата, а длина предназначена для обработки номеров строк на случай, если нужно прочитать несколько файлов.

Начнем с простого оператора if, который проверяет, пуст ли массив options, если нет, продолжаем; в противном случае мы возвращаем файл без каких-либо изменений.
Основываясь на тестировании cat в моей командной строке в Linux, я решил сделать второй оператор if. Он проверяет, существуют ли обе опции -n и -b в массиве options. Если это так, мы удаляем -n.

ОПЯТЬ, теперь варианты!

Теперь мы перебираем массив options и выполняем ряд проверок, чтобы определить, что делать.
Здесь я не знаю, как объяснить их все, потому что это займет довольно много слов…
Помните, когда я говорил вам, что проще иметь дело с файлами как с массивом строк? Вот почему теперь я могу зацикливаться на каждой строке и обновлять их по одной в зависимости от параметров, это в значительной степени то, что я делал со всеми параметрами.
Возможно, это неэффективно, но это дает Работа выполнена.

Попробуйте прочитать код каждой опции по одному, я обещаю, что они не такие сложные, как вы думаете.

Обновление файла, прочитанного на шагах № 1 и № 2:

Теперь, когда у нас все работает, как задумано, мы изменим последний фрагмент кода, чтобы он вызывал только что написанную функцию applyOptions(), а затем распечатывает файл.

# reading files 
if len(files) > 0: 
    length = 0
    for i in range(len(files)):
        with open(getPath(files[i]), 'r') as text_file:
            f = text_file.readlines()

        print("".join(applyOptions(f, length)), end = '')
        length += len(f)

Теперь вместо печати файла мы печатаем результат функции applyOptions(). Обратите внимание, что мы добавили переменную длины для хранения длины файла; это не повлияет на первый файл. Но если есть несколько файлов, это важная часть, чтобы заставить его работать так же, как Cat в командной строке.

Теперь собираем детали по порядку!
Окончательный вариант скрипта размещен на моем GitHub здесь:
https://github.com/shelldawg/dawgcat/blob/main/dawgcat.py

Вынос:

А. Начните с разбиения программы на небольшие шаги.
Б. Комментарии помогут вам понять код, когда вы вернетесь к нему в следующем десятилетии.
В. Python — это весело.

Вы человек!

Если вы дошли до этого места, спасибо, честно говоря, это было веселое испытание.
Увидимся в следующий раз! И не забывайте, что версия C скоро выйдет.

Shelldawg — https://github.com/shelldawg