Uso practico de bash: Ejecutar un comando sobre varios archivos
Tags: spanish
Published: 2022-07-06 18:24:35

Existen ocasiones en los que es necesario aplicar una serie de comandos en diferentes archivos, lo cual seria muy tedioso realizar manualmente, pero se puede lograr fácilmente con un poco de scripting en bash.

Vamos a empezar dando un ejemplo practico, tenemos un proyecto con la siguiente estructura:

```bash
.
├── Dockerfile
├── LICENSE
├── MANIFEST.in
├── README.md
├── VERSION
├── flake8.ini
├── mypy.ini
├── requirements_dev.txt
├── requirements_pkg.txt
├── setup.py
├── src
│   └── bodywork
│       ├── __init__.py
│       ├── cli
│       │   ├── __init__.py
│       │   ├── cli.py
│       │   ├── deployments.py
│       │   ├── secrets.py
│       │   ├── setup_namespace.py
│       │   ├── terminal.py
│       │   └── workflow_jobs.py
│       ├── config.py
│       ├── constants.py
│       ├── exceptions.py
│       ├── git.py
│       ├── k8s
│       │   ├── __init__.py
│       │   ├── auth.py
│       │   ├── batch_jobs.py
│       │   ├── deployments.py
│       │   ├── namespaces.py
│       │   ├── pod_logs.py
│       │   ├── secrets.py
│       │   ├── utils.py
│       │   └── workflow_jobs.py
│       ├── logs.py
│       ├── stage_execution.py
│       └── workflow_execution.py
├── structure.txt
├── tests
│   ├── conftest.py
│   ├── integration
│   │   ├── conftest.py
│   │   ├── test_git_integration.py
│   │   ├── test_k8s_with_cluster.py
│   │   └── test_k8s_with_secrets.py
│   ├── resources
│   │   └── project_repo
│   │       ├── bodywork.ini
│   │       ├── bodywork.yaml
│   │       ├── bodywork_bad_stages_section.yaml
│   │       ├── bodywork_batch_stage.yaml
│   │       ├── bodywork_empty.yaml
│   │       ├── bodywork_missing_sections.yaml
│   │       ├── on_fail_stage
│   │       │   └── main.py
│   │       ├── stage_1
│   │       │   └── main.py
│   │       ├── stage_2
│   │       │   └── main.py
│   │       ├── stage_3
│   │       │   └── main.py
│   │       ├── stage_4
│   │       │   └── main.py
│   │       ├── stage_5
│   │       │   └── main.py
│   │       └── stage_jupyter
│   │           └── main.ipynb
│   └── unit_and_functional
│       ├── conftest.py
│       ├── test_cli.py
│       ├── test_cli_deployments.py
│       ├── test_cli_secrets.py
│       ├── test_cli_setup_namespace.py
│       ├── test_cli_terminal.py
│       ├── test_cli_workflow_jobs.py
│       ├── test_config.py
│       ├── test_git.py
│       ├── test_k8s_auth.py
│       ├── test_k8s_batch_jobs.py
│       ├── test_k8s_deployments.py
│       ├── test_k8s_namespaces.py
│       ├── test_k8s_pod_logs.py
│       ├── test_k8s_secrets.py
│       ├── test_k8s_utils.py
│       ├── test_k8s_workflow_jobs.py
│       ├── test_logs.py
│       ├── test_stage_execution.py
│       └── test_workflow_execution.py
├── tox.ini
└── xclip

16 directories, 75 files
```

Nuestra tarea es cambiar todos los [Docstrings]([https://peps.python.org/pep-0257/](https://peps.python.org/pep-0257/)) dentro de los archivos python, a [Formato Google]([https://google.github.io/styleguide/pyguide.html](https://google.github.io/styleguide/pyguide.html)), esto podría llevarnos horas y es propenso a errores, ya que es probable que ni siquiera conozcamos todas las diferencias entre ambos formatos.

Para ello después de un poco de investigación, encontramos en github el paquete [Pyment]([https://github.com/dadadel/pyment](https://github.com/dadadel/pyment)), el cual resuelve el problema de tener que modificar los archivos de manera manual, sin embargo, si leemos en la documentación, podemos encontrar que para realizar esta tarea se requieren una serie de comandos sobre cada archivo:

```bash
# Generar el archivo patch
$ pyment test.py

# Aplicar patch al archivo original
$ patch -p1 test.py.patch

# Borrar archivo patch
$ rm test.py.patch
```

Si bien la cantidad de trabajo disminuye considerablemente, todavía es necesario encontrar cada archivo y ejecutar estos comandos, lo cual sigue consumiendo bastante tiempo ( sin mencionar lo tedioso que puede ser)

Para hacer esto aun mas sencillo, usaremos el siguiente comando:

```bash
$ find . -name '*.py' | xargs  -I % bash -c 'cd $(dirname %) ; pyment $(basename %); patch -p1 < $(basename %).patc
```

Dividamos este comando en varias para explicarlo mas fácilmente, primero que nada:

```bash
$ find . -name '*.py'
./setup.py
./src/bodywork/workflow_execution.py
./src/bodywork/__init__.py
./src/bodywork/config.py
./src/bodywork/logs.py
./src/bodywork/exceptions.py
./src/bodywork/constants.py
./src/bodywork/k8s/utils.py
./src/bodywork/k8s/pod_logs.py
./src/bodywork/k8s/deployments.py
./src/bodywork/k8s/__init__.py
./src/bodywork/k8s/batch_jobs.py
./src/bodywork/k8s/workflow_jobs.py
./src/bodywork/k8s/namespaces.py
./src/bodywork/k8s/secrets.py
./src/bodywork/k8s/auth.py
./src/bodywork/cli/cli.py
./src/bodywork/cli/deployments.py
./src/bodywork/cli/__init__.py
./src/bodywork/cli/setup_namespace.py
./src/bodywork/cli/terminal.py
./src/bodywork/cli/workflow_jobs.py
./src/bodywork/cli/secrets.py
./src/bodywork/stage_execution.py
./src/bodywork/git.py
./tests/unit_and_functional/test_k8s_batch_jobs.py
./tests/unit_and_functional/test_k8s_auth.py
./tests/unit_and_functional/test_cli_secrets.py
./tests/unit_and_functional/test_k8s_utils.py
./tests/unit_and_functional/test_cli_workflow_jobs.py
./tests/unit_and_functional/test_cli_setup_namespace.py
./tests/unit_and_functional/test_k8s_deployments.py
./tests/unit_and_functional/test_k8s_namespaces.py
./tests/unit_and_functional/test_k8s_pod_logs.py
./tests/unit_and_functional/test_cli_deployments.py
./tests/unit_and_functional/test_workflow_execution.py
./tests/unit_and_functional/test_cli_terminal.py
./tests/unit_and_functional/test_stage_execution.py
./tests/unit_and_functional/test_k8s_secrets.py
./tests/unit_and_functional/test_k8s_workflow_jobs.py
./tests/unit_and_functional/conftest.py
./tests/unit_and_functional/test_logs.py
./tests/unit_and_functional/test_config.py
./tests/unit_and_functional/test_cli.py
./tests/unit_and_functional/test_git.py
./tests/resources/project_repo/stage_3/main.py
./tests/resources/project_repo/stage_2/main.py
./tests/resources/project_repo/stage_4/main.py
./tests/resources/project_repo/stage_1/main.py
./tests/resources/project_repo/on_fail_stage/main.py
./tests/resources/project_repo/stage_5/main.py
./tests/integration/test_git_integration.py
./tests/integration/test_k8s_with_cluster.py
./tests/integration/conftest.py
./tests/integration/test_k8s_with_secrets.py
./tests/conftest.py
```

- El comando find regresa una lista de archivos que cumplan con los parámetros de búsqueda especificado
- Este comando require especificar el directorio a partir del cual se buscaran archivos, utilizamos `.` que es un alias para el directorio actual
- La bandera `-name '*.py'`  indica nuestro parámetro de búsqueda, que en este caso seran todos los archivos con terminación .py

Utilizaremos el operador pipe, que permite ingresar la salida de un comando como la entrada de un comando posterior, pasando asi nuestra lista de archivos al comando `xargs`

```bash
$ $ xargs  -I % bash -c 'cd $(dirname %) ; pyment $(basename %); patch -p1 < $(basename %).patc
```

- El comando xargs nos permite ejecutar un comando a cada uno de los elementos de una lista, en este caso, nos permite ejecutar un comando a cada uno de los archivos resultantes del comando anterior
- La bandera `-I %` Nos permite especificar un símbolo que sera utilizado como placeholder dentro del comando para representar el nombre del archivo.
- Los argumentos siguientes  `bash -c 'cd $(dirname %) ; pyment $(basename %); patch -p1 < $(basename %).patch; rm $(basename %).patch`  especifican el comando que sera utilizado en cada iteración:

```bash
# Cambiar al directorio del archivo
# Recordemos que el comando find nos devuele resultados de la siguiente manera
# ./src/bodywork/workflow_execution.py
# El comando dirname nos regresa el directorio de un archivo, removiendo
# el placeholder esto sera equivalente a 
# cd $(dirname ./src/bodywork/workflow_execution.py);
# cd ./src/bodywork/
$ cd $(dirname %);

# Una vez dentro del directorio ejecutamos el comando pyment sobre el archivo
# De igual manera, el comando basename regresa el nombre de un archivo,
# Sustituyendo el placeholder esto sera equivalente a
# pyment $(basename ./src/bodywork/workflow_execution.py);
# pyment workflow_execution.py
$ pyment $(basename %);

# Aplicar patch
# Sustituyendo el placeholder esto sera equivalente a
# $ patch -p1 < $(basename /src/bodywork/workflow_execution.py).patch'
# $ patch -p1 < workflow_execution.py.patch
$ patch -p1 < $(basename %).patch'

# Eliminar Archivo patch generado
$ rm $(basename %).patch

```