bash - programas - shell script linux español
Realice un comando desde la línea de comando dentro de los directorios desde glob en script bash shell (9)
En bash, puede ejecutar "set -o noglob" que inhibirá el shell para expandir nombres de ruta (globs). Pero esto debe establecerse en el shell en ejecución antes de ejecutar el script; de lo contrario, debe citar cualquier metacarácter que proporcione en los argumentos.
En un script de shell bash do-for.sh
quiero realizar un comando dentro de todos los directorios nombrados en un glob usando bash. Esto ha sido respondido montones de veces, pero quiero proporcionar el comando en la línea de comando. En otras palabras, suponiendo que tengo los directorios:
-
foo
-
bar
Quiero ingresar
do-for * pwd
y haz que bash imprima el directorio de trabajo dentro de foo
y luego dentro de la bar
.
Después de leer las innumerables respuestas en la web, pensé que podría hacer esto:
for dir in $1; do
pushd ${dir}
$2 $3 $4 $5 $6 $7 $8 $9
popd
done
¡Aparentemente, aunque el glob *
se expande en la otra variable de argumentos de la línea de comando! Así que, la primera vez en el ciclo, por $2 $3 $4 $5 $6 $7 $8 $9
¡Esperaba foo pwd
pero en cambio parece que me meto en foo bar
!
¿Cómo puedo evitar que el globo en la línea de comando se expanda a los otros parámetros? ¿O hay una mejor manera de abordar esto?
Para aclarar esto, aquí es cómo quiero usar el archivo por lotes. (Por cierto, esto funciona bien en la versión de archivo por lotes de Windows).
./do-for.sh repo-* git commit -a -m "Added new files."
En este caso, el problema no es la expansión de metacarácter, es solo que su script tiene un número indefinido de argumentos, de los cuales el último es el comando a ejecutar para todos los argumentos anteriores.
#!/bin/bash
CMND=$(eval echo "/${$#}") # get the command as last argument without arguments or
while [[ $# -gt 1 ]]; do # execute loop for each argument except last one
( cd "$1" && eval "$CMND" ) # switch to each directory received and execute the command
shift # throw away 1st arg and move to the next one in line
done
Uso: ./script.sh * pwd
o ./script.sh * "ls -l"
Para tener el comando seguido de argumentos (por ejemplo, / script.sh * ls -l) el script debe ser más largo porque cada argumento debe probarse si es un directorio hasta que se identifique el comando (o hacia atrás hasta que se identifique un directorio) )
Aquí hay una secuencia de comandos alternativa que aceptaría la sintaxis: ./script.sh <dirs...> <command> <arguments...>
Por ejemplo: ./script.sh * ls -la
# Move all dirs from args to DIRS array
typeset -i COUNT=0
while [[ $# -gt 1 ]]; do
[[ -d "$1" ]] && DIRS[COUNT++]="$1" && shift || break
done
# Validate that the command received is valid
which "$1" >/dev/null 2>&1 || { echo "invalid command: $1"; exit 1; }
# Execute the command + it''s arguments for each dir from array
for D in "${DIRS[@]}"; do
( cd "$D" && eval "$@" )
done
find-while-read
combinación find-while-read
es una de las combinaciones más seguras para analizar los nombres de archivo. Haz algo como debajo
#!/bin/bash
myfunc(){
cd "$2"
eval "$1" # Execute the command parsed as an argument
}
cur_dir=$(pwd) # storing the current directory
find . -type d -print0 | while read -rd '''' dname
do
myfunc "pwd" "$dname"
cd "$cur_dir" #Remember myfunc changes the current working dir, so you need this
done
Así es como lo haría:
#!/bin/bash
# Read directory arguments into dirs array
for arg in "$@"; do
if [[ -d $arg ]]; then
dirs+=("$arg")
else
break
fi
done
# Remove directories from arguments
shift ${#dirs[@]}
cur_dir=$PWD
# Loop through directories and execute command
for dir in "${dirs[@]}"; do
cd "$dir"
"$@"
cd "$cur_dir"
done
Esto pasa por los argumentos como se ve después de la expansión, y siempre que sean directorios, se agregan a la matriz de directorios. Tan pronto como se encuentre el primer argumento que no sea de directorio, asumiremos que ahora se inicia el comando.
Los directorios se eliminan de los argumentos con shift
y almacenamos el directorio actual en cur_dir
.
El último ciclo visita cada directorio y ejecuta el comando que consiste en el resto de los argumentos.
Esto funciona para tu
./do-for.sh repo-* git commit -a -m "Added new files."
Por ejemplo, pero si repo-*
expande a cualquier otra cosa que no sea un directorio, el script se rompe porque intentará ejecutar el nombre del archivo como parte del comando.
Podría hacerse más estable si, por ejemplo, el globo y el comando estuvieran separados por un indicador como --
, pero si usted sabe que el globo siempre será solo un directorio, esto debería funcionar.
Asumiré que está abierto a que los usuarios tengan que proporcionar algún tipo de separador, como
./do-for.sh repo-* -- git commit -a -m "Added new files."
Su secuencia de comandos podría hacer algo como (esto es solo para explicar el concepto, no he probado el código real):
CURRENT_DIR="$PWD"
declare -a FILES=()
for ARG in "$@"
do
[[ "$ARG" != "--" ]] || break
FILES+=("$ARG")
shift
done
if
[[ "${1-}" = "--" ]]
then
shift
else
echo "You must terminate the file list with -- to separate it from the command"
(return, exit, whatever you prefer to stop the script/function)
fi
En este punto, tiene todos los archivos de destino en una matriz, y "$ @" contiene solo el comando para ejecutar. Todo lo que queda por hacer es:
for FILE in "${FILES[@]-}"
do
cd "$FILE"
"$@"
cd "$CURRENT_DIR"
done
Tenga en cuenta que esta solución tiene la ventaja de que si su usuario olvida el separador "-", se le notificará (en lugar de un error debido a las cotizaciones).
¿Por qué no mantenerlo simple y crear una función de shell que utiliza find
pero facilita la carga para los usuarios de tipear sus comandos, por ejemplo:
do_for() { find . -type d /( ! -name . /) -not -path ''*//.*'' -name $1 -exec bash -c "cd ''{}'' && "${@:2}" " /; }
Entonces pueden escribir algo como do_for repo-* git commit -a -m "Added new files."
Tenga en cuenta que si quiere usar * por sí mismo, tendrá que escapar de él:
do_for /* pwd
Los comodines son evaluados por el shell antes de pasarlos a cualquier programa o script. No hay nada que puedas hacer al respecto.
Pero si acepta citar la expresión globbing entonces este script debería hacer el truco
#!/usr/bin/env bash
for dir in $1; do (
cd "$dir"
"${@:2}"
) done
Lo probé con dos directorios de prueba y parece estar funcionando. Úselo así:
mkdir test_dir1 test_dir2
./do-for.sh "test_dir*" git init
./do-for.sh "test_dir*" touch test_file
./do-for.sh "test_dir*" git add .
./do-for.sh "test_dir*" git status
./do-for.sh "test_dir*" git commit -m "Added new files."
¿Nadie propone una solución usando find
? ¿Por qué no probar algo como esto?
find . -type d /( -wholename ''YOURPATTERN'' /) -print0 | xargs -0 YOURCOMMAND
Mira man find
para más opciones.
Comenzaré con el archivo por lotes de Windows que mencionó dos veces como activo. La gran diferencia es que en Windows, el shell no hace ningún globbing, dejándolo a los diversos comandos (y cada uno de ellos lo hace de forma diferente), mientras que en Linux / Unix el globbing generalmente lo hace el shell, y puede ser prevenido citando o escapando. Tanto el enfoque de Windows como el de Linux tienen sus ventajas, y se comparan de manera diferente en diferentes casos de uso.
Para usuarios regulares de bash, citando
./do-for.sh repo-''*'' git commit -a -m "Added new files."
o escapando
./do-for.sh repo-/* git commit -a -m "Added new files."
son la solución más simple, porque son lo que utilizan sistemáticamente a diario. Si sus usuarios necesitan una sintaxis diferente, tiene todas las soluciones propuestas hasta ahora, que clasificaré en cuatro categorías antes de proponer la mía (tenga en cuenta que en cada ejemplo a continuación, do-for.sh
representa una secuencia de comandos diferente que adopta la solución respectiva, que se puede encontrar en una de las otras respuestas).
Desactivar el globbing de shell. Esto es torpe, porque, incluso si recuerda qué opción de shell lo hace, debe recordar reiniciarlo para que el shell funcione normalmente después.
Use un separador:
./do-for.sh repo-* -- git commit -a -m "Added new files."
Esto funciona, es similar a la solución adoptada en situaciones similares con otros comandos de shell, y falla solo si su expansión de nombres de directorio incluye un nombre de directorio exactamente igual al separador (un evento improbable, que no sucedería en el ejemplo anterior, pero en general podría suceder.)
Tener el comando como último argumento, el resto son directorios:
./do-for.sh repo-* ''git commit -a -m "Added new files."''
Esto funciona, pero una vez más, implica citar, posiblemente incluso anidar, y no tiene sentido preferirlo a la cita más habitual de caracteres globbing.
Intenta ser inteligente:
./do-for.sh repo-* git commit -a -m "Added new files."
y considere manejar directorios hasta que llegue a un nombre que no sea un directorio. Esto funcionaría en muchos casos, pero podría fallar de manera oscura (por ejemplo, cuando tienes un directorio llamado como el comando).
Mi solución no pertenece a ninguna de las categorías mencionadas. De hecho, lo que propongo es no usar *
como un personaje globbing en el primer argumento de tu script. (Esto es similar a la sintaxis utilizada por el comando split
en el que proporciona un argumento de prefijo no globbed para los archivos que se generarán). Tengo dos versiones (código a continuación). Con la primera versión, harías lo siguiente:
# repo- is a prefix: the command will be excuted in all
# subdirectories whose name starts with it
./do-for.sh repo- git commit -a -m "Added new files."
# The command will be excuted in all subdirectories
# of the current one
./do-for.sh . git commit -a -m "Added new files."
# If you want the command to be executed in exactly
# one subdirectory with no globbing at all,
# ''/'' can be used as a ''stop character''. But why
# use do-for.sh in this case?
./do-for.sh repo/ git commit -a -m "Added new files."
# Use ''.'' to disable the stop character.
# The command will be excuted in all subdirectories of the
# given one (paths have to be always relative, though)
./do-for.sh repos/. git commit -a -m "Added new files."
La segunda versión implica el uso de un carácter globbing del que el shell no conoce nada, como el carácter %
de SQL
# the command will be excuted in all subdirectories
# matching the SQL glob
./do-for.sh repo-% git commit -a -m "Added new files."
./do-for.sh user-%-repo git commit -a -m "Added new files."
./do-for.sh % git commit -a -m "Added new files."
La segunda versión es más flexible, ya que permite globos no finales, pero es menos estándar para el mundo bash.
Aquí está el código:
#!/bin/bash
if [ "$#" -lt 2 ]; then
echo "Usage: ${0##*/} PREFIX command..." >&2
exit 1
fi
pathPrefix="$1"
shift
### For second version, comment out the following five lines
case "$pathPrefix" in
(*/) pathPrefix="${pathPrefix%/}" ;; # Stop character, remove it
(*.) pathPrefix="${pathPrefix%.}*" ;; # Replace final dot with glob
(*) pathPrefix+=/* ;; # Add a final glob
esac
### For second version, uncomment the following line
# pathPrefix="${pathPrefix//%/*}" # Add a final glob
tmp=${pathPrefix//[^//]} # Count how many levels down we have to go
maxDepth=$((1+${#tmp}))
# Please note that this won’t work if matched directory names
# contain newline characters (comment added for those bash freaks who
# care about extreme cases)
declare -a directories=()
while read d; do
directories+=("$d")
done < <(find . -maxdepth "$maxDepth" -path ./"$pathPrefix" -type d -print)
curDir="$(pwd)"
for d in "${directories[@]}"; do
cd "$d";
"$@"
cd "$curDir"
done
Al igual que en Windows, aún necesitaría usar comillas si el prefijo contiene espacios
./do-for.sh ''repository for project'' git commit -a -m "Added new files."
(pero si el prefijo no contiene espacios, puede evitar citarlo y tratará correctamente cualquier nombre de directorio que contenga espacio que comience con ese prefijo; con cambios obvios, lo mismo es cierto para% -patterns en la segunda versión).
Tenga en cuenta las otras diferencias relevantes entre un entorno Windows y Linux, como la distinción entre mayúsculas y minúsculas en los nombres de las rutas, las diferencias entre los caracteres que se consideran especiales, etc.