Hur kan vi köra ett kommando lagrat i en variabel?

$ ls -l /tmp/test/my\ dir/ total 0 

Jag undrade varför följande sätt att köra ovanstående kommando misslyckas eller lyckas?

$ abc="ls -l "/tmp/test/my dir"" $ $abc ls: cannot access ""/tmp/test/my": No such file or directory ls: cannot access "dir"": No such file or directory $ "$abc" bash: ls -l "/tmp/test/my dir": No such file or directory $ bash -c $abc "my dir" $ bash -c "$abc" total 0 $ eval $abc total 0 $ eval "$abc" total 0 

Kommentarer

Svar

Detta har diskuterats i ett antal frågor om unix.SE, jag försöker samla alla frågor som jag kan komma med här. Referenser i slutet.


Varför det misslyckas

Anledningen till att du möter dessa problem är orduppdelning och det faktum att citat utökade från variabler fungerar inte som citat, utan är bara vanliga tecken.

fall som presenteras i frågan:

Tilldelningen här tilldelar den enskilda strängen ls -l "/tmp/test/my dir" till abc:

$ abc="ls -l "/tmp/test/my dir"" 

Nedan $abc delas i det vita utrymmet och ls får de tre argumenten -l, "/tmp/test/my och dir" (med en offert på framsidan av den andra och en på baksidan av den tredje). Alternativet fungerar, men sökvägen behandlas felaktigt:

$ $abc ls: cannot access ""/tmp/test/my": No such file or directory ls: cannot access "dir"": No such file or directory 

Här citeras expansionen, så den hålls som ett enda ord. Skalet försöker för att hitta ett program som heter ls -l "/tmp/test/my dir", mellanslag och citat inkluderade.

$ "$abc" bash: ls -l "/tmp/test/my dir": No such file or directory 

Och här, $abc delas och endast det första resulterande ordet tas som argument till -c, så Bash kör bara ls i den aktuella katalogen. De andra orden är argument att bash och används för att fylla $0, $1 osv.

$ bash -c $abc "my dir" 

Med bash -c "$abc" och eval "$abc" finns det ytterligare steg för bearbetning av skal, vilket får offerten att fungera, men orsakar också att alla skalutvidgningar bearbetas igen så det finns en risk för att oavsiktligt körs, t.ex. en kommandosättning från användarinformation, såvida du inte ” är mycket försiktig ful om att citera.


Bättre sätt att göra det

De två bättre sätten att lagra ett kommando är a) använda en funktion istället, b) använda en arrayvariabel ( eller positionsparametrarna.

Använda en funktion:

Förklara helt enkelt en funktion med kommandot inuti och kör funktionen som om det vore ett kommando. Utvidgningar av kommandon inom funktionen bearbetas bara när kommandot körs, inte när det är definierat och du inte behöver citera de enskilda kommandona.

# define it myls() { ls -l "/tmp/test/my dir" } # run it myls 

Med hjälp av en matris:

Arrays gör det möjligt att skapa flera ordvariabler där de enskilda orden innehåller vita Plats. Här lagras de enskilda orden som distinkta matriselement, och "${array[@]}" expansion expanderar varje element som separata skalord:

# define the array mycmd=(ls -l "/tmp/test/my dir") # run the command "${mycmd[@]}" 

Syntaxen är lite hemsk men med arrays kan du också bygga kommandoraden bit för bit. Till exempel:

mycmd=(ls) # initial command if [ "$want_detail" = 1 ]; then mycmd+=(-l) # optional flag fi mycmd+=("$targetdir") # the filename "${mycmd[@]}" 

eller håll delar av kommandoraden konstant och använd arrayen för att bara fylla en del av den, som alternativ eller filnamn:

options=(-x -v) files=(file1 "file name with whitespace") target=/somedir transmutate "${options[@]}" "${files[@]}" "$target" 

Nackdelen med matriser är att de inte är en standardfunktion, så vanliga POSIX-skal (som dash, standard /bin/sh i Debian / Ubuntu) stöder inte dem (men se nedan). Bash, ksh och zsh gör dock, så det är troligt att ditt system har något skal som stöder matriser.

Användning av "$@"

I skal utan stöd för namngivna matriser kan man fortfarande använda positionsparametrarna (pseudo-array "$@") för att hålla argumenten för ett kommando.

Följande bör vara bärbara skriptbitar som motsvarar kodbitarna i föregående avsnitt. Matrisen ersätts med "$@", listan över positionsparametrar. Inställning av "$@" görs med set och de dubbla citaten runt "$@" är viktiga (dessa gör att elementen i listan citeras individuellt).

Lagra först ett kommando med argument i "$@" och kör det:

set -- ls -l "/tmp/test/my dir" "$@" 

Villkorligt ställa in delar av kommandoradsalternativen för ett kommando:

set -- ls if [ "$want_detail" = 1 ]; then set -- "$@" -l fi set -- "$@" "$targetdir" "$@" 

Använd bara "$@" för alternativ och operander :

set -- -x -v set -- "$@" file1 "file name with whitespace" set -- "$@" /somedir transmutate "$@" 

(Naturligtvis fylls "$@" vanligtvis med argumenten till själva skriptet, så du ” Jag måste spara dem någonstans innan du ändrar om "$@".)


Var försiktig med eval !

Eftersom eval introducerar en extra nivå av offert- och utvidgningsbehandling, måste du vara försiktig med användarinmatning. Det fungerar till exempel så länge användaren skriver inte in några enskilda citat:

read -r filename cmd="ls -l "$filename"" eval "$cmd"; 

Men om de ger inmatningen "$(uname)".txt, kommer ditt skript gärna kör kommandosubstitutionen.

En version med arrays är immun mot th eftersom orden hålls åtskilda under hela tiden finns det inget citat eller annan bearbetning för innehållet i filename.

read -r filename cmd=(ls -ld -- "$filename") "${cmd[@]}" 

Referenser

Kommentarer

  • du kan komma runt det evala citat genom att göra cmd="ls -l $(printf "%q" "$filename")". inte vackert, men om användaren är död på att använda en eval hjälper det. Det ’ är också mycket användbart för att skicka kommandot genom liknande saker, till exempel ssh foohost "ls -l $(printf "%q" "$filename")", eller i sprit av denna fråga: ssh foohost "$cmd".
  • Inte direkt relaterat, men har du hårdkodat katalogen? I så fall kanske du vill titta på alias. Något som: $ alias abc='ls -l "/tmp/test/my dir"'

Svar

Det säkraste sättet att kör ett (icke-trivialt) kommando är eval. Sedan kan du skriva kommandot som du skulle göra på kommandoraden och det körs exakt som om du precis hade angett det. Men du måste citera allt.

Enkelt fall:

abc="ls -l "/tmp/test/my dir"" eval "$abc" 

inte så enkelt fall:

# command: awk "! a[$0]++ { print "foo: " $0; }" inputfile abc="awk "\""! a[$0]++ { print "foo: " $0; }"\"" inputfile" eval "$abc" 

Kommentarer

  • Hej @HaukeLaging, vad är \ ’ ’?
  • @jumping_monkey Du kan inte ha en ' i en ' -citerad sträng . Således måste du (1) avsluta / avbryta citatet, (2) undkomma nästa ' för att få det bokstavligt, (3) skriv bokstavligt citat och (4) i vid ett avbrott återuppta den citerade strängen: (1)'(2)\(3)'(4)'
  • Hej @HaukeLaging, intressant, och varför inte bara abc='awk \'! a[$0]++ { print "foo: " $0; }\' inputfile' ?
  • @jumping_monkey För att inte tala om att jag just förklarade varför det inte fungerar: Skulle ’ inte vara vettigt att testa koden innan du skickar den?

Svar

Det andra citattecknet bryter kommandot.

När jag kör:

abc="ls -l "/home/wattana/Desktop"" $abc 

Det gav mig ett fel.

Men när jag kör

abc="ls -l /home/wattana/Desktop" $abc 

Det finns inget fel alls

Det finns inget sätt att åtgärda det just nu (för mig) men du kan undvika felet genom att inte ha plats i katalognamnet.

Detta svar sa att eval-kommandot kan användas för att åtgärda detta men det fungerar inte för mig 🙁

Kommentarer

  • Ja, det fungerar så länge som ’ inget behov av t.ex. filnamn med inbäddade mellanslag (eller sådana som innehåller globtecken).

Svar

Om detta inte fungerar med "", då ska du använda ``:

abc=`ls -l /tmp/test/my\ dir/` 

Uppdatera bättre way:

abc=$(ls -l /tmp/test/my\ dir/) 

Kommentarer

  • Detta lagrar resultatet av kommandot i en variabel. OP: n vill (konstigt) spara själva kommandot i en variabel. Åh, och du bör verkligen börja använda $( command... ) istället för backticks.
  • Tack så mycket för förtydligandena och tipsen!

Svar

Vad sägs om en python3 one-liner?

bash-4.4# pwd /home bash-4.4# CUSTOM="ls -altr" bash-4.4# python3 -c "import subprocess; subprocess.call(\"$CUSTOM\", shell=True)" total 8 drwxr-xr-x 2 root root 4096 Mar 4 2019 . drwxr-xr-x 1 root root 4096 Nov 27 16:42 .. 

Kommentarer

  • Frågan handlar om BASH, inte python.

Svar

Ytterligare ett enkelt knep för att köra alla (triviala / icke-triviala) kommandon lagrade i abc variabel är:

$ history -s $abc 

och tryck UpArrow eller Ctrl-p för att ta det på kommandoraden. Till skillnad från andra metoder på det här sättet kan du redigera den före körning om det behövs.

Detta kommando lägger till variabelt innehåll som ny post i Bash-historiken och du kan komma ihåg det genom UpArrow.

Lämna ett svar

Din e-postadress kommer inte publiceras. Obligatoriska fält är märkta *