Hoe kunnen we een commando uitvoeren dat is opgeslagen in een variabele?

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

Ik vroeg me af waarom de volgende manieren om de bovenstaande opdracht uit te voeren mislukken of slagen?

$ 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 

Reacties

Antwoord

Dit is besproken in een aantal vragen over unix.SE, ik zal proberen om alle problemen die ik hier kan bedenken. Verwijzingen aan het einde.


Waarom het mislukt

De reden dat u met deze problemen wordt geconfronteerd, is woordsplitsing en het feit dat aanhalingstekens die zijn uitgebreid vanuit variabelen niet “fungeren als aanhalingstekens, maar gewoon gewone tekens zijn.

De gevallen gepresenteerd in de vraag:

De toewijzing hier wijst de enkele string toe ls -l "/tmp/test/my dir" naar abc:

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

Hieronder $abc is opgesplitst in witruimte, en ls krijgt de drie argumenten -l, "/tmp/test/my en dir" (met een aanhalingsteken vooraan in de tweede en een ander aan de achterkant van de derde). De optie werkt, maar het pad wordt verkeerd verwerkt:

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

Hier wordt de uitbreiding geciteerd, dus het wordt als een enkel woord gehouden. De shell probeert om een programma te vinden dat letterlijk ls -l "/tmp/test/my dir" heet, inclusief spaties en aanhalingstekens.

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

En hier, $abc is gesplitst en alleen het eerste resulterende woord wordt als argument gebruikt voor -c, dus Bash voert gewoon ls in de huidige directory. De andere woorden zijn argumenten voor bash, en worden gebruikt om $0, $1, enz. te vullen. p>

$ bash -c $abc "my dir" 

Met bash -c "$abc", en eval "$abc" is er een extra shell-verwerkingsstap, waardoor de aanhalingstekens werken, maar zorgt er ook voor dat alle shell-uitbreidingen opnieuw worden verwerkt , dus er is “een risico dat u per ongeluk bijvoorbeeld een opdrachtvervanging uitvoert vanuit door de gebruiker verstrekte gegevens, tenzij u” opnieuw erg zorgzaam ful over citeren.


Betere manieren om het te doen

De twee betere manieren om een commando op te slaan zijn a) gebruik in plaats daarvan een functie, b) gebruik een arrayvariabele ( of de positionele parameters).

Een functie gebruiken:

Verklaar eenvoudig een functie met het commando erin, en voer de functie uit alsof het een commando is. Uitbreidingen in commandos binnen de functie worden alleen verwerkt als het commando wordt uitgevoerd, niet als het “is gedefinieerd, en je hoeft de individuele commandos niet te citeren.

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

Een array gebruiken:

Met arrays kunnen variabelen met meerdere woorden worden gemaakt waarbij de afzonderlijke woorden wit bevatten ruimte. Hier worden de individuele woorden opgeslagen als afzonderlijke array-elementen, en de "${array[@]}" -uitbreiding breidt elk element uit als afzonderlijke shell-woorden:

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

De syntaxis is enigszins vreselijk, maar met arrays kun je de opdrachtregel ook stukje bij beetje opbouwen. Bijvoorbeeld:

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

of houd delen van de opdrachtregel constant en gebruik de array om slechts een deel ervan te vullen, zoals opties of bestandsnamen:

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

Het nadeel van arrays is dat ze “geen standaardfunctie zijn, dus gewone POSIX-shells (zoals dash, de standaard /bin/sh in Debian / Ubuntu) ondersteunen ze niet (maar zie hieronder). Bash, ksh en zsh doen dat echter wel, dus het is waarschijnlijk dat uw systeem een shell heeft die arrays ondersteunt.

Met "$@"

In shells zonder ondersteuning voor benoemde arrays kan men nog steeds de positionele parameters gebruiken (de pseudo-array "$@") om de argumenten van een commando vast te houden.

De volgende zouden draagbare scriptbits moeten zijn die het equivalent doen van de codebits in de vorige sectie. De array is vervangen door "$@", de lijst met positionele parameters. Het instellen van "$@" gebeurt met set, en de dubbele aanhalingstekens rond "$@" zijn belangrijk (deze zorgen ervoor dat de elementen van de lijst afzonderlijk worden geciteerd).

Ten eerste, gewoon een commando met argumenten opslaan in "$@" en het uitvoeren:

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

Voorwaardelijk delen van de opdrachtregelopties instellen voor een commando:

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

Alleen "$@" gebruiken voor opties en operanden :

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

(Natuurlijk wordt "$@" meestal gevuld met de argumenten voor het script zelf, dus u ” Ik zal ze ergens moeten opslaan voordat ik "$@" opnieuw gebruik.)


Wees voorzichtig met eval !

Aangezien eval een extra niveau van verwerking van citaten en uitbreidingen introduceert, moet u voorzichtig zijn met gebruikersinvoer. Dit werkt bijvoorbeeld zolang de gebruiker typt geen enkele aanhalingstekens:

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

Maar als ze de invoer "$(uname)".txt geven, zal je script graag voert de opdrachtsubstitutie uit.

Een versie met arrays is immuun voor th aangezien de woorden de hele tijd gescheiden worden gehouden, is er geen aanhalingsteken of andere verwerking voor de inhoud van filename.

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

Verwijzingen

Reacties

  • je kunt het eval citeren omzeilen door cmd="ls -l $(printf "%q" "$filename")" te doen. niet mooi, maar als de gebruiker dood is in het gebruik van een eval, helpt het. Het ‘ is ook erg handig voor het verzenden van de opdracht door soortgelijke dingen, zoals ssh foohost "ls -l $(printf "%q" "$filename")", of in de geest van deze vraag: ssh foohost "$cmd".
  • Niet direct gerelateerd, maar heb je de directory hard gecodeerd? In dat geval wilt u misschien naar alias kijken. Iets als: $ alias abc='ls -l "/tmp/test/my dir"'

Antwoord

De veiligste manier om voer een (niet-triviaal) commando uit is eval. Vervolgens kunt u het commando schrijven zoals u zou doen op de commandoregel en het wordt precies uitgevoerd alsof u het net had ingevoerd. Maar je moet alles citeren.

Eenvoudig geval:

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

niet zo eenvoudig geval:

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

Reacties

  • Hallo @HaukeLaging, wat is \ ‘ ‘?
  • @jumping_monkey Je kunt geen ' hebben binnen een ' -citaat string . Je moet dus (1) het citeren beëindigen / onderbreken, (2) ontsnappen aan de volgende ' om het letterlijk te krijgen, (3) het letterlijke citaat typen en (4) in geval van een onderbreking, hervat de geciteerde string: (1)'(2)\(3)'(4)'
  • Hallo @HaukeLaging, interessant, en waarom niet gewoon abc='awk \'! a[$0]++ { print "foo: " $0; }\' inputfile' ?
  • @jumping_monkey Laat staan dat ik je zojuist heb uitgelegd waarom dat niet werkt: Zou het ‘ niet zinvol zijn om code te testen voordat je deze plaatst?

Answer

Het tweede aanhalingsteken breekt de opdracht af.

Wanneer ik voer:

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

Ik kreeg een foutmelding.

Maar toen ik

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

Er is helemaal geen fout

Er is momenteel (voor mij) geen manier om dit op te lossen, maar je kunt de fout vermijden door geen ruimte in de mapnaam te hebben.

Dit antwoord zei dat het eval-commando kan worden gebruikt om dit op te lossen, maar het werkt niet voor mij 🙁

Reacties

  • Ja, dat werkt zolang er ‘ s geen b.v. bestandsnamen met ingesloten spaties (of degenen die glob-tekens bevatten).

Answer

Als dit niet werkt met "", gebruik dan ``:

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

Update beter way:

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

Reacties

  • Dit slaat het resultaat van het commando op in een variabele. Het OP wil (vreemd genoeg) het commando zelf in een variabele opslaan. Oh, en je zou echt $( command... ) moeten gaan gebruiken in plaats van backticks.
  • Heel erg bedankt voor de verduidelijkingen en tips!

Antwoord

Wat dacht je van een 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 .. 

Opmerkingen

  • De vraag gaat over BASH, niet over python.

Answer

Nog een simpele truc om een (triviaal / niet-triviaal) commando uit te voeren dat is opgeslagen in abc variabele is:

$ history -s $abc 

en druk op UpArrow of Ctrl-p om het naar de opdrachtregel te brengen. In tegenstelling tot elke andere methode kun je op deze manier het bewerken voordat het wordt uitgevoerd, indien nodig.

Dit commando zal de variabele inhoud als nieuw item toevoegen aan de Bash-geschiedenis en je kunt het oproepen met UpArrow.

Geef een reactie

Het e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *