Come vengono interpretati gli script di comando di Windows (CMD.EXE)?

Mi sono imbattuto in ss64.com che fornisce un buon aiuto su come scrivere script batch che verrà eseguito nell’interprete Command di Windows.

Tuttavia, non sono stato in grado di trovare una buona spiegazione della grammatica degli script batch, di come le cose si espandono o non si espandono e di come sfuggire alle cose.

Ecco domande esemplificative che non sono stato in grado di risolvere:

  • Come viene gestito il sistema di quote? Ho creato uno script TinyPerl
    ( foreach $i (@ARGV) { print '*' . $i ; } ), lo ha compilato e lo ha chiamato in questo modo:
    • my_script.exe "a ""b"" c" → l’output è *a "b*c
    • my_script.exe """abc""" → output it *"a*b*c"
  • Come funziona il comando echo interno? Cosa viene espanso all’interno di quel comando?
  • Perché devo usare for [...] %%I negli script di file, ma for [...] %I nelle sessioni interattive?
  • Quali sono i personaggi di fuga e in quale contesto? Come sfuggire a un segno di percentuale? Ad esempio, come posso echo %PROCESSOR_ARCHITECTURE% letteralmente? Ho trovato che echo.exe %""PROCESSOR_ARCHITECTURE% funziona, c’è una soluzione migliore?
  • Come corrispondono le coppie di % ? Esempio:
    • set b=a , echo %a %b% c%%aac%
    • set a =b , echo %a %b% c%bb c%
  • Come faccio a garantire che una variabile passi a un comando come un singolo argomento se mai questa variabile contiene virgolette doppie?
  • Come vengono memorizzate le variabili quando si utilizza il comando set ? Ad esempio, se set a=a" b e quindi echo.%a% ottengo a" b . Se comunque uso echo.exe da UnxUtils, ottengo ab . In che modo %a% espande in un modo diverso?

Grazie per le tue luci.

Ho eseguito molti esperimenti per studiare la grammatica degli script batch. Ho anche studiato le differenze tra la modalità batch e la riga di comando.

Il parser di riga batch:

L’elaborazione di una riga di codice in un file batch coinvolge più fasi.

Ecco una breve panoramica delle varie fasi:

Fase 0) Lettura linea:

Fase 1) Espansione percentuale:

Fase 1.5) Rimuovi : rimuove tutti i caratteri di ritorno a capo (0x0D)

Fase 2) Elaborare caratteri speciali, tokenize e creare un blocco di comandi memorizzato nella cache: si tratta di un processo complesso influenzato da elementi quali virgolette, caratteri speciali, delimitatori di token e fughe di caret.

Fase 3) Eco dei comandi analizzati Solo se il blocco di comando non inizia con @ e ECHO era ON all’inizio del passaggio precedente.

Fase 4) PER Espansione variabile %X : solo se è attivo un comando FOR e vengono elaborati i comandi dopo DO.

Fase 5) Espansione ritardata: solo se l’espansione ritardata è abilitata

Fase 5.3) Elaborazione dei tubi: solo se i comandi si trovano su entrambi i lati di una tubazione

Fase 5.5) Esegui reindirizzamento:

Fase 6) Elaborazione CALL / Raddoppio Caret: solo se il token di comando è CALL

Fase 7) Esegui: il comando viene eseguito


E qui ci sono i dettagli per ogni fase:

Si noti che le fasi descritte di seguito sono solo un modello di come funziona il parser batch. Gli interni effettivi di cmd.exe potrebbero non riflettere queste fasi. Ma questo modello è efficace nel predire il comportamento degli script batch.

Fase 0) Riga di lettura : legge la riga di input.

  • Quando si legge una riga da analizzare come un comando, (0x1A) viene letto come (LineFeed 0x0A)
  • Quando GOTO o CALL legge le righe durante la scansione di:: label, , viene trattato come se stesso – non viene convertito in

Fase 1) Espansione percentuale:

  • Una doppia %% è sostituita da una singola %
  • Espansione delle variabili argomento ( %1 , %2 , ecc.)
  • Espansione di %var% , se var non esiste sostituirlo con niente
  • Per una spiegazione completa leggere la prima metà di questo da dbenham Same thread: Percent Phase

Fase 1.5) Rimuovi : Rimuovi tutti i ritorni a capo (0x0D) dalla linea

Fase 2) Elaborare caratteri speciali, tokenize e creare un blocco di comandi memorizzato nella cache: si tratta di un processo complesso influenzato da elementi quali virgolette, caratteri speciali, delimitatori di token e fughe di caret. Quello che segue è un’approssimazione di questo processo.

Ci sono alcuni concetti che sono importanti in questa fase.

  • Un token è semplicemente una stringa di caratteri che viene trattata come un’unità.
  • I token sono separati da delimitatori di token. I delimitatori di token standard sono ; , = <0x0B> <0x0C> e <0xFF>
    I delimitatori di token consecutivi vengono considerati come uno: non vi sono token vuoti tra i delimitatori di token
  • Non ci sono delimitatori di token all’interno di una stringa quotata. L’intera stringa citata viene sempre considerata come parte di un singolo token. Un singolo token può consistere in una combinazione di stringhe tra virgolette e caratteri non quotati.

I seguenti caratteri possono avere un significato speciale in questa fase, a seconda del contesto: ^ ( @ & | < > ; = <0x0B> <0x0C> <0xFF>

Guarda ogni personaggio da sinistra a destra:

  • Se è un segno di omissione ( ^ ), il carattere successivo viene scappato e il caret di escape viene rimosso. I personaggi in fuga perdono tutti i significati speciali (ad eccezione di ).
  • Se si tratta di una citazione ( " ), triggersre il flag di quotazione Se il flag di quotatura è attivo, solo " e sono speciali. Tutti gli altri personaggi perdono il loro significato speciale fino a quando la quotazione successiva non distriggers la virgola. Non è ansible sfuggire alla citazione di chiusura. Tutti i caratteri citati sono sempre nello stesso token.
  • distriggers sempre il flag di quota. Altri comportamenti variano a seconda del contesto, ma le virgolette non alterano mai il comportamento di .
    • Escaped
      • è spogliato
      • Il prossimo personaggio è scappato. Se alla fine del buffer di riga, la riga successiva viene letta e aggiunta a quella corrente prima di sfuggire al carattere successivo. Se il carattere successivo è , viene trattato come letterale, il che significa che questo processo non è ricorsivo.
    • Senza escape non tra parentesi
      • viene rimosso e l'analisi della riga corrente viene interrotta.
      • Qualsiasi carattere rimanente nel buffer di riga viene semplicemente ignorato.
    • Senza escape all'interno di un blocco FOR FOR parentesi
      • è convertito in
      • Se alla fine del buffer di riga, la riga successiva viene letta e aggiunta a quella corrente.
    • Senza escape all'interno di un blocco comandi tra parentesi
      • viene convertito in e lo viene trattato come parte della riga successiva del blocco di comandi.
      • Se al buffer di fine riga, la riga successiva viene letta e aggiunta allo spazio.
  • Se è uno dei personaggi speciali & | < o > , dividere la linea a questo punto per gestire le pipe, la concatenazione dei comandi e il reindirizzamento.
    • Nel caso di una pipe ( | ), ogni lato è un comando separato (o un blocco di comando) che ottiene una gestione speciale nella fase 5.3
    • Nel caso di & , && , o || concatenazione del comando, ogni lato della concatenazione viene trattato come un comando separato.
    • Nel caso di < , << , > , o >> reindirizzamento, la clausola di reindirizzamento viene analizzata, temporaneamente rimossa e quindi aggiunta alla fine del comando corrente. Una clausola di reindirizzamento consiste in una cifra di handle di file facoltativa, l'operatore di reindirizzamento e il token di destinazione di reindirizzamento.
      • Se il token che precede l'operatore di reindirizzamento è una singola cifra, la cifra specifica l'handle del file da redirect. Se il token di handle non viene trovato, il reindirizzamento dell'output viene impostato su 1 (stdout) e i valori predefiniti di reindirizzamento dell'input su 0 (stdin).
  • Se il primo token per questo comando (prima di spostare il reindirizzamento alla fine) inizia con @ , quindi @ ha un significato speciale. ( @ non è speciale in nessun altro contesto)
    • Lo speciale @ è stato rimosso.
    • Se ECHO è attivo, questo comando, insieme ai seguenti comandi concatenati su questa linea, viene escluso dall'eco di fase 3. Se @ è prima di un'apertura ( , quindi l'intero blocco tra parentesi viene escluso dall'eco di fase 3.
  • Parentesi di processo (fornisce istruzioni composte su più righe):
    • Se il parser non sta cercando un token di comando, allora ( non è speciale.
    • Se il parser sta cercando un comando token e trova ( , quindi avvia una nuova istruzione composta e incrementa il contatore parentesi
    • Se il contatore di parentesi è> 0, allora termina l'istruzione composta e decrementa il contatore di parentesi.
    • Se viene raggiunta la fine della linea e il contatore della parentesi è> 0, la riga successiva verrà aggiunta all'istruzione composta (ricomincia con la fase 0)
    • Se il contatore di parentesi è 0 e il parser sta cercando un comando, allora ) funziona in modo simile a un'istruzione REM a condizione che sia immediatamente seguito da un delimitatore di token, carattere speciale, nuova riga o fine del file
      • Tutti i caratteri speciali perdono il loro significato eccetto ^ (è ansible la concatenazione delle righe)
      • Una volta raggiunta la fine della linea logica, l'intero "comando" viene scartato.
  • Ogni comando viene analizzato in una serie di token. Il primo token viene sempre considerato come un token di comando (dopo che lo speciale @ è stato eliminato e il reindirizzamento è spostato verso la fine).
    • I delimitatori di token iniziali prima del token di comando vengono eliminati
    • Quando si analizza il token di comando, ( funziona come un delimitatore di token di comando, oltre ai delimitatori di token standard
    • La gestione dei token successivi dipende dal comando.
  • Molti comandi concatenano semplicemente tutti gli argomenti dopo il comando token in un singolo token argomento. Tutti i delimitatori di token degli argomenti vengono mantenuti. Le opzioni degli argomenti in genere non vengono analizzate fino alla fase 7.
  • Tre comandi ottengono una gestione speciale - IF, FOR e REM
    • SE è diviso in due o tre parti distinte che vengono elaborate in modo indipendente. Un errore di syntax nella costruzione IF provocherà un errore di syntax fatale.
      • L'operazione di confronto è l'effettivo comando che scorre fino alla fase 7
        • Tutte le opzioni IF sono completamente analizzate nella fase 2.
        • I delimitatori di token consecutivi collassano in un unico spazio.
        • A seconda dell'operatore di confronto, saranno identificati uno o due token di valore.
      • Il vero blocco di comando è l'insieme di comandi dopo la condizione e viene analizzato come qualsiasi altro blocco di comandi. Se si utilizza ELSE, il blocco True deve essere tra parentesi.
      • Il comando opzionale False è l'insieme di comandi dopo ELSE. Di nuovo, questo blocco di comandi viene analizzato normalmente.
      • I blocchi di comandi Vero e Falso non fluiscono automaticamente nelle fasi successive. La loro successiva elaborazione è controllata dalla fase 7.
    • FOR è diviso in due dopo il DO. Un errore di syntax nella costruzione FOR provocherà un errore di syntax fatale.
      • La parte attraverso DO è l'effettivo comando di iterazione che scorre fino alla fase 7
        • Tutte le opzioni FOR sono completamente analizzate nella fase 2.
        • La clausola IN parentesi considera come . Dopo aver analizzato la clausola IN, tutti i token vengono concatenati insieme per formare un singolo token.
        • I delimitatori di token consecutivi senza escape / non quotati collassano in un unico spazio attraverso il comando FOR attraverso DO.
      • La parte dopo DO è un blocco di comando che viene analizzato normalmente. L'elaborazione successiva del blocco di comando DO è controllata dall'iterazione nella fase 7.
    • Il REM rilevato nella fase 2 viene trattato in modo molto diverso rispetto a tutti gli altri comandi.
      • Viene analizzato solo un token argomento: il parser ignora i caratteri dopo il primo token argomento.
      • Se esiste un solo token argomento che termina con un ^ escape che termina la linea, il token dell'argomento viene gettato via e la riga successiva viene analizzata e aggiunta al REM. Questo si ripete finché non c'è più di un token, o l'ultimo carattere non è ^ .
      • Il comando REM può apparire nell'output di fase 3, ma il comando non viene mai eseguito e il testo dell'argomento originale viene riprodotto - i caret di escape non vengono rimossi.
  • Se il token di comando inizia con:, e questo è il primo round della fase 2 (non un riavvio a causa di CALL nella fase 6) quindi
    • Il token viene normalmente considerato come un'etichetta non eseguita .
      • Il resto della riga viene analizzato, tuttavia ) , < , > , & e | non ha più un significato speciale. L'intero resto della riga è considerato parte dell'etichetta "comando".
      • Il ^ continua ad essere speciale, il che significa che la continuazione della linea può essere utilizzata per aggiungere la riga successiva all'etichetta.
      • Un'etichetta non eseguita all'interno di un blocco tra parentesi provocherà un errore di syntax fatale a meno che non sia immediatamente seguito da un comando o etichetta eseguita nella riga successiva.
        • Si noti che ( non ha più un significato speciale per il primo comando che segue l' etichetta Unexecuted in questo contesto.
      • Il comando viene interrotto al termine dell'analisi dell'etichetta. Le fasi successive non hanno luogo per l'etichetta
    • Esistono tre eccezioni che possono causare un'etichetta trovata nella fase 2 da trattare come un'etichetta esecutata che continua l'analisi attraverso la fase 7.
      • C'è il reindirizzamento che precede il token dell'etichetta e c'è un | pipe o & , && , o || comando concatenazione sulla linea.
      • C'è un reindirizzamento che precede il token dell'etichetta e il comando si trova all'interno di un blocco tra parentesi.
      • Il token label è il primo comando su una riga all'interno di un blocco tra parentesi e la riga sopra è terminata con un'etichetta Unexecuted .
    • Quanto segue si verifica quando viene rilevata un'etichetta esecutiva nella fase 2
      • L'etichetta, i suoi argomenti e il suo reindirizzamento sono tutti esclusi da qualsiasi output di eco nella fase 3
      • Tutti i successivi comandi concatenati sulla riga sono completamente analizzati ed eseguiti.
    • Per ulteriori informazioni su Etichette Eseguite e Etichette non Eseguite , vedere https://www.dostips.com/forum/viewtopic.php?f=3&t=3803&p=55405#p55405

Fase 3) Eco dei comandi analizzati Solo se il blocco di comando non inizia con @ e ECHO era ON all'inizio del passaggio precedente.

Fase 4) PER Espansione variabile %X : solo se è attivo un comando FOR e vengono elaborati i comandi dopo DO.

  • A questo punto, la fase 1 dell'elaborazione batch avrà già convertito una variabile FOR come %%X in %X La riga di comando ha diverse regole di espansione percentuale per la fase 1. Questo è il motivo per cui le righe di comando utilizzano %X ma i file batch utilizzano %%X per le variabili FOR.
  • Per i nomi delle variabili si fa distinzione tra maiuscole e minuscole, ma i ~modifiers non sono case sensitive.
  • ~modifiers hanno la precedenza sui nomi delle variabili. Se un carattere che segue ~ è sia un modificatore che un nome di variabile FOR valido, e esiste un carattere successivo che è un nome di variabile FOR attivo, il carattere viene interpretato come un modificatore.
  • PER I nomi delle variabili sono globali, ma solo nel contesto di una clausola DO. Se una routine viene richiamata da una clausola FOR DO, le variabili FOR non vengono espanse all'interno della routine CALL. Ma se la routine ha il proprio comando FOR, allora tutte le variabili FOR attualmente definite sono accessibili ai comandi DO interni.
  • PER i nomi delle variabili possono essere riutilizzati all'interno di FOR annidati. Il valore FOR interno ha la precedenza, ma una volta chiuso INNER FOR, viene ripristinato il valore FOR esterno.
  • Se ECHO era attivo all'inizio di questa fase, la fase 3 viene ripetuta per mostrare i comandi DO analizzati dopo che le variabili FOR sono state espanse.

---- Da questo punto in poi, ogni comando identificato nella fase 2 viene elaborato separatamente.
---- Le fasi da 5 a 7 sono completate per un comando prima di passare a quello successivo.

Fase 5) Espansione ritardata: solo se l'espansione ritardata è triggers

  • Se il comando si trova all'interno di un blocco tra parentesi su entrambi i lati di una pipe, saltare questo passaggio.
  • Ogni token per un comando viene analizzato per l'espansione ritardata in modo indipendente.
    • La maggior parte dei comandi analizza due o più token: il token di comando, il token degli argomenti e ciascun token di destinazione di reindirizzamento.
    • Il comando FOR analizza solo il token della clausola IN.
    • Il comando IF analizza solo i valori di confronto, uno o due, a seconda dell'operatore di confronto.
  • Per ogni token analizzato, prima controlla se ne contiene ! . In caso contrario, il token non viene analizzato, importante per ^ caratteri. Se il token contiene ! , quindi scansiona ciascun carattere da sinistra a destra:
    • Se è un segno di omissione ( ^ ) il prossimo carattere non ha alcun significato speciale, il cursore stesso viene rimosso
    • Se è un punto esclamativo, cerca il prossimo punto esclamativo (i carnet non vengono più visualizzati), espandi al valore della variabile.
      • Apertura consecutiva ! sono crollati in un solo !
      • Qualsiasi rimanente ! quello che non può essere accoppiato viene rimosso
    • Importante: in questa fase le virgolette e altri caratteri speciali vengono ignorati
    • L'espansione dei vars in questa fase è "sicura", perché i caratteri speciali non vengono più rilevati (anche o )
    • Per una spiegazione più completa, leggi la seconda parte di questo da dbenham same thread - Exclamation Point Phase
    • Ci sono alcuni casi limite in cui queste regole sembrano fallire:
      Vedi L'espansione ritardata fallisce in alcuni casi

Fase 5.3) Elaborazione dei tubi: solo se i comandi si trovano su entrambi i lati di una tubazione
Ogni lato del tubo viene elaborato indipendentemente.

  • Se si ha a che fare con un blocco comandi tra parentesi, allora tutti i con un comando prima e dopo vengono convertiti in & . Altri sono spogliati.
  • Il comando (o il blocco di comando) viene eseguito in modo asincrono in un nuovo thread cmd.exe tramite
    %comspec% /S /D /c" commandBlock" . Ciò significa che il blocco di comando ottiene un riavvio di fase, ma questa volta in modalità riga di comando.
  • Questa è la fine dell'elaborazione per i comandi di pipe.
  • Per maggiori informazioni su come vengono analizzati ed elaborati i tubi, guarda questa domanda e le risposte: Perché l'espansione ritardata fallisce quando si trova all'interno di un blocco di codice convogliato?

Fase 5.5) Esegui reindirizzamento: viene ora eseguito qualsiasi reindirizzamento rilevato nella fase 2.

  • I risultati delle fasi 4 e 5 possono influire sul reindirizzamento rilevato nella fase 2.
  • Se il reindirizzamento non riesce, il resto del comando viene interrotto. Si noti che il reindirizzamento non riuscito non imposta ERRORLEVEL su 1 a meno che || è usato

Fase 6) Elaborazione CALL / Raddoppio Caret: solo se il token comando è CALL, o se il testo che precede il primo delimitatore token standard è CALL. Se CALL viene analizzato da un token di comando più grande, la parte non utilizzata viene anteposta al token degli argomenti prima di procedere.

  • Scansiona il token degli argomenti per un non quotato /? . Se si trova ovunque all'interno dei token, quindi interrompere la fase 6 e passare alla fase 7, dove verrà stampato HELP for CALL.
  • Rimuovi la prima CALL , quindi è ansible impilare più CALL
  • Raddoppia tutti i segnali
  • Riavvia le fasi 1, 1.5 e 2, ma non continuare con la fase 3
    • Eventuali doppioni raddoppiati vengono ridotti a un punto di omissione finché non vengono citati. Ma sfortunatamente, i carnet citati rimangono raddoppiati.
    • Il blocco di comandi memorizzato nella cache è già stato preparato nel round originale della fase 2. Così molte delle attività della fase 2 sono state modificate
      • Viene rilevato qualsiasi nuovo reindirizzamento non ridiretto, senza escape che non è stato rilevato nel primo round della fase 2, ma viene rimosso (incluso il nome del file) senza eseguire effettivamente il reindirizzamento
      • Ogni nuovo accenno non accennato, senza curvatura, alla fine della linea viene rimosso senza eseguire la continuazione della linea
      • La chiamata viene interrotta senza errori se viene rilevato uno dei seguenti elementi
        • Recentemente apparendo non quotato, senza caratteri di escape & o |
        • Il token comando risultante inizia con unquot, senza caratteri di escape (
        • Il primo token dopo la CALL rimossa è iniziato con @
      • Se il comando risultante è un IF o FOR apparentemente valido, l'esecuzione fallirà successivamente con un errore che indica che IF o FOR non sono riconosciuti come comando interno o esterno.
      • Ovviamente il CALL non viene interrotto in questo secondo round della fase 2 se il token comando risultante è un'etichetta che inizia con :
    • Ci sono alcuni casi limite in cui queste regole falliscono:
      Vedi Esame dei Linefeed con CALL
  • Se il token comando risultante è CALL, quindi riavvia la Fase 6 (si ripete fino a quando non viene più CHIAMATA)
  • Se il token comando risultante è uno script batch o un: label, l'esecuzione di CALL viene completamente gestita dal resto della fase 6.
    • Spingere la posizione del file di script batch corrente sullo stack di chiamata in modo che l'esecuzione possa riprendere dalla posizione corretta al termine della CALL.
    • Imposta i token argomento% 0,% 1,% 2, ...% N e% * per la CHIAMATA, utilizzando tutti i token risultanti
    • Se il token di comando è un'etichetta che inizia con:, quindi
      • Riavvia la fase 5. Ciò può influire su cosa: l'etichetta è CHIAMATA. Ma dal momento che i token% 0, ecc. Sono già stati impostati, non modificherà gli argomenti passati alla routine CALL.
      • Esegui l'etichetta GOTO per posizionare il puntatore del file all'inizio della subroutine (ignora tutti gli altri token che possono seguire l'etichetta:) Vedi la fase 7 per le regole su come funziona GOTO.
        • Se manca il token label o l'etichetta: non viene trovata, lo stack delle chiamate viene immediatamente interrotto per ripristinare la posizione del file salvato e CALL viene interrotta.
        • Se l'etichetta: compare contiene / ?, allora viene stampato l'aiuto di GOTO invece di cercare l'etichetta:. Il puntatore del file non si sposta, in modo tale che il codice dopo che CALL è stato eseguito due volte, una volta nel contesto CALL e quindi nuovamente dopo il ritorno di CALL. Vedi Perché CALL stampa il messaggio di aiuto GOTO in questo script e perché il comando dopo viene eseguito due volte? per maggiori informazioni.
    • Altro controllo del trasferimento allo script batch specificato.
    • Esecuzione di CALLed: l'etichetta o lo script continua fino a quando non viene raggiunto EXIT / B o end-of-file, a quel punto lo stack CALL viene interrotto e l'esecuzione riprende dalla posizione del file salvato.
      La fase 7 non viene eseguita per script CALL o: etichette.
  • Altrimenti il ​​risultato della fase 6 ricade nella fase 7 per l'esecuzione.

Fase 7) Esegui: il comando viene eseguito

  • 7.1 - Esegui comando interno - Se il token di comando è quotato, saltare questo passaggio. Altrimenti, tenta di analizzare un comando interno ed eseguirlo.
    • Vengono eseguiti i seguenti test per determinare se un token di comando non quotato rappresenta un comando interno:
      • Se il token di comando corrisponde esattamente a un comando interno, quindi eseguirlo.
      • Altrimenti interromperà il token di comando prima della prima occorrenza di + / [ ] ; o =
        Se il testo precedente è un comando interno, ricorda quel comando
        • Se in modalità riga di comando, o se il comando proviene da un blocco tra parentesi, blocco di comando IF true o false, blocco di comando FOR DO o coinvolto con concatenazione di comandi, eseguire il comando interno
        • Altrimenti (deve essere un comando autonomo in modalità batch) esegue la scansione della cartella corrente e del PERCORSO per un file .COM, .EXE, .BAT o .CMD il cui nome base corrisponde al token del comando originale
          • Se il primo file corrispondente è un .BAT o .CMD, goto 7.3.exec ed esegui lo script
          • Altrimenti (la corrispondenza non trovata o la prima corrispondenza è .EXE o .COM) esegue il comando interno memorizzato
      • Altrimenti interromperà il token di comando prima della prima occorrenza di . \ o :
        Se il testo precedente non è un comando interno, goto 7.2
        Altrimenti il ​​testo precedente potrebbe essere un comando interno. Ricorda questo comando.
      • Rompere il token di comando prima della prima occorrenza di + / [ ] ; o =
        Se il testo precedente è un percorso di un file esistente, quindi vai a 7.2
        Altrimenti esegui il comando interno memorizzato.
    • Se un comando interno viene analizzato da un token di comando più grande, la parte non utilizzata del token di comando è inclusa nell'elenco degli argomenti
    • Solo perché un token di comando viene analizzato come un comando interno non significa che verrà eseguito correttamente. Ogni comando interno ha le proprie regole su come vengono analizzati gli argomenti e le opzioni e quale syntax è consentita.
    • Tutti i comandi interni stamperanno la guida invece di eseguire la loro funzione se /? viene rilevato La maggior parte riconosce /? se appare ovunque negli argomenti. Ma alcuni comandi come ECHO e SET stampano solo aiuto se il primo token argomento inizia con /? .
    • SET ha alcune semantiche interessanti:
      • Se un comando SET ha una citazione prima del nome della variabile
        set "name=content" ignored -> valore = content
        quindi il testo tra il primo segno di uguale e l'ultima citazione viene utilizzato come contenuto (prima uguale e ultima citazione esclusa). Il testo dopo l'ultima citazione viene ignorato. Se non c'è alcuna citazione dopo il segno di uguale, il resto della riga viene utilizzato come contenuto.
      • Se un comando SET non ha una citazione prima del nome
        set name="content" not ignored -> value = "content" not ignored
        quindi l'intero resto della riga dopo l'uguale viene utilizzato come contenuto, incluse tutte le citazioni eventualmente presenti.
    • Viene valutato un confronto IF e, a seconda che la condizione sia vera o falsa, viene elaborato il blocco di comando dipendente già analizzato, a partire dalla fase 5.
    • La clausola IN di un comando FOR viene iterata in modo appropriato.
      • Se questo è un FOR / F che itera l'output di un blocco di comando, allora:
        • La clausola IN viene eseguita in un nuovo processo cmd.exe tramite CMD / C.
        • Il blocco di comandi deve passare attraverso l'intero processo di analisi una seconda volta, ma questa volta in un contesto della riga di comando
        • ECHO si avvia su ON e l'espansione ritardata di solito inizia disabilitata (dipende dalle impostazioni del registro)
        • Tutte le modifiche all'ambiente apportate dal blocco di comandi della clausola IN andranno perse quando termina il processo child cmd.exe
      • Per ogni iterazione:
        • I valori delle variabili FOR sono definiti
        • Il blocco di comando DO già analizzato viene quindi elaborato, a partire dalla fase 4.
    • GOTO utilizza la seguente logica per individuare l'etichetta:
      • L'etichetta viene analizzata dal primo token argomento
      • Lo script viene scansionato per la prossima occorrenza dell'etichetta
        • La scansione inizia dalla posizione del file corrente
        • Se viene raggiunta la fine del file, la scansione torna all'inizio del file e continua fino al punto iniziale originale.
      • La scansione si arresta alla prima occorrenza dell'etichetta che trova e il puntatore del file viene impostato sulla linea immediatamente successiva all'etichetta. L'esecuzione dello script riprende da quel punto. Si noti che un vero GOTO corretto interromperà immediatamente qualsiasi blocco di codice analizzato, inclusi i loop FOR.
      • Se l'etichetta non viene trovata o manca il token dell'etichetta, GOTO non riesce, viene stampato un messaggio di errore e viene eseguito lo schiocco delle chiamate. Funziona in modo efficace come EXIT / B, tranne che i comandi già analizzati nel blocco di comandi corrente che seguono GOTO sono ancora eseguiti, ma nel contesto di CALLer (il contesto esistente dopo EXIT / B)
      • Vedere https://www.dostips.com/forum/viewtopic.php?f=3&t=3803 per una descrizione più precisa delle regole utilizzate per l'analisi delle etichette.
    • RENAME e COPY accettano entrambi i caratteri jolly per i percorsi di origine e di destinazione. Ma Microsoft fa un lavoro terribile documentando come funzionano i caratteri jolly, specialmente per il percorso target. Un utile set di regole jolly può essere trovato in Come il comando RENAME di Windows interpreta i caratteri jolly?
  • 7.2 - Esegui cambio volume - Altrimenti se il token comando non inizia con una citazione, è lungo esattamente due caratteri e il 2 ° carattere è due punti, quindi cambia il volume
    • Tutti i token argomento sono ignorati
    • Se il volume specificato dal primo carattere non può essere trovato, quindi interrompere con un errore
    • Un token comando di :: genererà sempre un errore, a meno che non venga utilizzato SUBST per definire un volume per ::
      Se SUBST viene utilizzato per definire un volume per :: , il volume verrà modificato, non verrà considerato come un'etichetta.
  • 7.3 - Esegui comando esterno - Else prova a trattare il comando come un comando esterno.
    • Se il 2 ° carattere del token di comando è due punti, verificare che il volume specificato dal 1 ° carattere sia trovato.
      Se il volume non può essere trovato, quindi interrompere con un errore.
    • Se in modalità batch e il token di comando inizia con:, quindi vai a 7.4
      Si noti che se il token dell'etichetta inizia con :: , non verrà raggiunto poiché il passaggio precedente verrà annullato con un errore, a meno che non venga utilizzato SUBST per definire un volume per :: .
    • Identifica il comando esterno da eseguire.
      • Questo è un processo complesso che può coinvolgere il volume corrente, la directory corrente, la variabile PATH, la variabile PATHEXT e le associazioni di file.
      • Se un comando esterno valido non può essere identificato, quindi interrompere con un errore.
    • Se in modalità riga di comando e il token di comando inizia con:, quindi vai a 7.4
      Notare che questo è raramente raggiunto perché il passaggio precedente verrà interrotto con un errore a meno che il token di comando non inizi con :: , e SUBST sia usato per definire un volume per :: , e l'intero token di comando è un percorso valido per un comando esterno .
    • 7.3.exec - Esegue il comando esterno.
  • 7.4 - Ignora un'etichetta - Ignora il comando e tutti i suoi argomenti se il token di comando inizia con :
    Le regole in 7.2 e 7.3 possono impedire a un'etichetta di raggiungere questo punto.

Command Line Parser:

Funziona come il parser BatchLine, ad eccezione di:

Fase 1) Espansione percentuale:

  • %var% è ancora sostituito dal contenuto di var, ma se var non è definito, l'espressione sarà invariata.
  • Nessuna gestione speciale di %% . Se var = content, allora %%var%% espande in %content% .

Fase 3) Eco dei comandi analizzati

  • Questo non viene eseguito dopo la fase 2. Viene eseguito solo dopo la fase 4 per il blocco di comando FOR DO.

Fase 5) Espansione ritardata: solo se DelayedExpansion è abilitato

  • !var! è ancora sostituito dal contenuto di var, ma se var non è definito, l'espressione sarà invariata.

Fase 7) Esegui comando

  • Tenta di CALL o GOTO a: l'etichetta genera un errore.
  • Anche se non è ansible chiamare le etichette, una riga valida potrebbe contenere ancora un'etichetta. Come già documentato nella fase 7, un'etichetta eseguita può causare un errore in diversi scenari.
    • Le etichette eseguite in batch possono solo causare un errore se iniziano con ::
    • Le etichette eseguite dalla riga di comando causano quasi sempre un errore

Analisi di valori interi

Esistono molti contesti diversi in cui cmd.exe analizza i valori interi da stringhe e le regole sono incoerenti:

  • SET /A
  • IF
  • %var:~n,m% (espansione della sottostringa variabile)
  • FOR /F "TOKENS=n"
  • FOR /F "SKIP=n"
  • FOR /L %%A in (n1 n2 n3)
  • EXIT [/B] n

I dettagli per queste regole sono disponibili in Regole per il modo in cui CMD.EXE analizza i numeri


Per chiunque desideri migliorare queste regole, c'è un argomento di discussione sul forum DosTips in cui è ansible segnalare i problemi e fare dei suggerimenti.

Spero che sia d'aiuto
Jan Erik (jeb) - Autore originale e scopritore delle varie fasi
Dave Benham (dbenham) - Molto contenuto e modifica aggiuntivi

Quando si richiama un comando da una finestra di comando, la tokenizzazione degli argomenti della riga di comando non viene eseguita da cmd.exe (ovvero “la shell”). Molto spesso la tokenizzazione viene eseguita dal runtime C / C ++ dei processi appena formati, ma questo non è necessariamente così – per esempio, se il nuovo processo non è stato scritto in C / C ++, o se il nuovo processo sceglie di ignorare argv e elaborare la riga di comando non elaborata per se stessa (ad es. con GetCommandLine () ). A livello di sistema operativo, Windows passa le righe di comando askenizzate come una singola stringa ai nuovi processi. This is in contrast to most *nix shells, where the shell tokenizes arguments in a consistent, predictable way before passing them to the newly formsd process. All this means that you may experience wildly divergent argument tokenization behavior across different programs on Windows, as individual programs often take argument tokenization into their own hands.

If it sounds like anarchy, it kind of is. However, since a large number of Windows programs do utilize the Microsoft C/C++ runtime’s argv , it may be generally useful to understand how the MSVCRT tokenizes arguments. Here is an excerpt:

  • Arguments are delimited by white space, which is either a space or a tab.
  • A string surrounded by double quotation marks is interpreted as a single argument, regardless of white space contained within. A quoted string can be embedded in an argument. Note that the caret (^) is not recognized as an escape character or delimiter.
  • A double quotation mark preceded by a backslash, \”, is interpreted as a literal double quotation mark (“).
  • Backslashes are interpreted literally, unless they immediately precede a double quotation mark.
  • If an even number of backslashes is followed by a double quotation mark, then one backslash () is placed in the argv array for every pair of backslashes (\), and the double quotation mark (“) is interpreted as a string delimiter.
  • If an odd number of backslashes is followed by a double quotation mark, then one backslash () is placed in the argv array for every pair of backslashes (\) and the double quotation mark is interpreted as an escape sequence by the remaining backslash, causing a literal double quotation mark (“) to be placed in argv.

The Microsoft “batch language” ( .bat ) is no exception to this anarchic environment, and it has developed its own unique rules for tokenization and escaping. It also looks like cmd.exe’s command prompt does do some preprocessing of the command line argument (mostly for variable substitution and escaping) before passing the argument off to the newly executing process. You can read more about the low-level details of the batch language and cmd escaping in the excellent answers by jeb and dbenham on this page.


Let’s build a simple command line utility in C and see what it says about your test cases:

 int main(int argc, char* argv[]) { int i; for (i = 0; i < argc; i++) { printf("argv[%d][%s]\n", i, argv[i]); } return 0; } 

(Notes: argv[0] is always the name of the executable, and is omitted below for brevity. Tested on Windows XP SP3. Compiled with Visual Studio 2005.)

 > test.exe "a ""b"" c" argv[1][a "b" c] > test.exe """abc""" argv[1]["abc"] > test.exe "a"" bc argv[1][a" bc] 

And a few of my own tests:

 > test.exe a "b" c argv[1][a] argv[2][b] argv[3][c] > test.exe a "bc" "de argv[1][a] argv[2][bc] argv[3][de] > test.exe a \"b\" c argv[1][a] argv[2]["b"] argv[3][c] 

Here is an expanded explanation of Phase 1 in jeb’s answer (Valid for both batch mode and command line mode).

Phase 1) Percent Expansion Starting from left, scan each character for % . If found then

  • 1.1 (escape % ) skipped if command line mode
    • If batch mode and followed by another % then
      Replace %% with single % and continue scan
  • 1.2 (expand argument) skipped if command line mode
    • Else if batch mode then
      • If followed by * and command extensions are enabled then
        Replace %* with the text of all command line arguments (Replace with nothing if there are no arguments) and continue scan.
      • Else if followed by then
        Replace % with argument value (replace with nothing if undefined) and continue scan.
      • Else if followed by ~ and command extensions are enabled then
        • If followed by optional valid list of argument modifiers followed by required then
          Replace %~[modifiers] with modified argument value (replace with nothing if not defined or if specified $PATH: modifier is not defined) and continue scan.
          Note: modifiers are case insensitive and can appear multiple times in any order, except $PATH: modifier can only appear once and must be the last modifier before the
        • Else invalid modified argument syntax raises fatal error: All parsed commands are aborted, and batch processing aborts if in batch mode!
  • 1.3 (expand variable)
    • Else if command extensions are disabled then
      Look at next string of characters, breaking before % or , and call them VAR (may be an empty list)
      • If next character is % then
        • If VAR is defined then
          Replace %VAR% with value of VAR and continue scan
        • Else if batch mode then
          Remove %VAR% and continue scan
        • Else goto 1.4
      • Else goto 1.4
    • Else if command extensions are enabled then
      Look at next string of characters, breaking before % : or , and call them VAR (may be an empty list). If VAR breaks before : and the subsequent character is % then include : as the last character in VAR and break before % .
      • If next character is % then
        • If VAR is defined then
          Replace %VAR% with value of VAR and continue scan
        • Else if batch mode then
          Remove %VAR% and continue scan
        • Else goto 1.4
      • Else if next character is : then
        • If VAR is undefined then
          • If batch mode then
            Remove %VAR: and continue scan.
          • Else goto 1.4
        • Else if next character is ~ then
          • If next string of characters matches pattern of [integer][,[integer]]% then
            Replace %VAR:~[integer][,[integer]]% with substring of value of VAR (possibly resulting in empty string) and continue scan.
          • Else goto 1.4
        • Else if followed by = or *= then
          Invalid variable search and replace syntax raises fatal error: All parsed commands are aborted, and batch processing aborts if in batch mode!
        • Else if next string of characters matches pattern of [*]search=[replace]% , where search may include any set of characters except = and , and replace may include any set of characters except % and , then replace
          %VAR:[*]search=[replace]% with value of VAR after performing search and replace (possibly resulting in empty string) and continue scan
        • Else goto 1.4
  • 1.4 (strip %)
    • Else If batch mode then
      Remove % and continue with scan
    • Else preserve % and continue scan

The above helps explain why this batch

 @echo off setlocal enableDelayedExpansion set "1var=varA" set "~f1var=varB" call :test "arg1" exit /b :: :test "arg1" echo %%1var%% = %1var% echo ^^^!1var^^^! = !1var! echo -------- echo %%~f1var%% = %~f1var% echo ^^^!~f1var^^^! = !~f1var! exit /b 

Gives these results:

 %1var% = "arg1"var !1var! = varA -------- %~f1var% = P:\arg1var !~f1var! = varB 

Note 1 – Phase 1 occurs prior to the recognition of REM statements. This is very important because it means even a remark can generate a fatal error if it has invalid argument expansion syntax or invalid variable search and replace syntax!

 @echo off rem %~x This generates a fatal argument expansion error echo this line is never reached 

Note 2 – Another interesting consequence of the % parsing rules: Variables containing : in the name can be defined, but they cannot be expanded unless command extensions are disabled. There is one exception – a variable name containing a single colon at the end can be expanded while command extensions are enabled. However, you cannot perform substring or search and replace operations on variable names ending with a colon. The batch file below (courtesy of jeb) demonstrates this behavior

 @echo off setlocal set var=content set var:=Special set var::=double colon set var:~0,2=tricky set var::~0,2=unfortunate echo %var% echo %var:% echo %var::% echo %var:~0,2% echo %var::~0,2% echo Now with DisableExtensions setlocal DisableExtensions echo %var% echo %var:% echo %var::% echo %var:~0,2% echo %var::~0,2% 

Note 3 – An interesting outcome of the order of the parsing rules that jeb lays out in his post: When performing search and replace with normal expansion, special characters should NOT be escaped (though they may be quoted). But when performing search and replace with delayed expansion, special characters MUST be escaped (unless they are quoted).

 @echo off setlocal enableDelayedExpansion set "var=this & that" echo %var:&=and% echo "%var:&=and%" echo !var:^&=and! echo "!var:&=and!" 

Here is an expanded, and more accurate explanation of phase 5 in jeb’s answer (Valid for both batch mode and command line mode)

Note there are some edge cases where these rules fail:
See Examination of Linefeeds with CALL

Phase 5) Delayed Expansion Only if delayed expansion is enabled, and the line contains at least one ! , then Starting from left, scan each character for ^ or ! , and if found, then

  • 5.1 (caret escape) Needed for ! or ^ literals
    • If character is a caret ^ then
      • Remove the ^
      • Scan the next character and preserve it as a literal
      • Continue the scan
  • 5.2 (expand variable)
    • If character is ! , then
      • If command extensions are disabled then
        Look at next string of characters, breaking before ! or , and call them VAR (may be an empty list)
        • If next character is ! poi
          • If VAR is defined, then
            Replace !VAR! with value of VAR and continue scan
          • Else if batch mode then
            Remove !VAR! and continue scan
          • Else goto 5.2.1
        • Else goto 5.2.1
      • Else if command extensions are enabled then
        Look at next string of characters, breaking before ! , : , or , and call them VAR (may be an empty list). If VAR breaks before : and the subsequent character is ! then include : as the last character in VAR and break before !
        • If next character is ! poi
          • If VAR exists, then
            Replace !VAR! with value of VAR and continue scan
          • Else if batch mode then
            Remove !VAR! and continue scan
          • Else goto 5.2.1
        • Else if next character is : then
          • If VAR is undefined then
            • If batch mode then
              Remove !VAR: and continue scan
            • Else goto 5.2.1
          • Else if next string of characters matches pattern of
            ~[integer][,[integer]]! poi
            Replace !VAR:~[integer][,[integer]]! with substring of value of VAR (possibly resulting in an empty string) and continue scan
          • Else if next string of characters matches pattern of [*]search=[replace]! , where search may include any set of characters except = and , and replace may include any set of characters except ! and , then
            Replace !VAR:[*]search=[replace]! with value of VAR after performing search and replace (possibly resulting in an empty string) and continue scan
          • Else goto 5.2.1
        • Else goto 5.2.1
      • 5.2.1
        • If batch mode then remove the !
          Else preserve the !
        • Continue the scan starting with the next character after the !

As pointed out, commands are passed the entire argument string in μSoft land, and it is up to them to parse this into separate arguments for their own use. There is no consistencty in this between different programs, and therefore there is no one set of rules to describe this process. You really need to check each corner case for whatever C library your program uses.

As far as the system .bat files go, here is that test:

 c> type args.cmd @echo off echo cmdcmdline:[%cmdcmdline%] echo 0:[%0] echo *:[%*] set allargs=%* if not defined allargs goto :eof setlocal @rem Wot about a nice for loop? @rem Then we are in the land of delayedexpansion, !n!, call, etc. @rem Plays havoc with args like %t%, a"b etc. ugh! set n=1 :loop echo %n%:[%1] set /a n+=1 shift set param=%1 if defined param goto :loop endlocal 

Now we can run some tests. See if you can figure out just what μSoft are trying to do:

 C>args abc cmdcmdline:[cmd.exe ] 0:[args] *:[abc] 1:[a] 2:[b] 3:[c] 

Fine so far. (I’ll leave out the uninteresting %cmdcmdline% and %0 from now on.)

 C>args *.* *:[*.*] 1:[*.*] 

No filename expansion.

 C>args "ab" c *:["ab" c] 1:["ab"] 2:[c] 

No quote stripping, though quotes do prevent argument splitting.

 c>args ""ab" c *:[""ab" c] 1:[""a] 2:[b" c] 

Consecutive double quotes causes them to lose any special parsing abilities they may have had. @Beniot’s example:

 C>args "a """ b "" c""" *:["a """ b "" c"""] 1:["a """] 2:[b] 3:[""] 4:[c"""] 

Quiz: How do you pass the value of any environment var as a single argument (ie, as %1 ) to a bat file?

 c>set t=a "bc c>set t t=a "bc c>args %t% 1:[a] 2:["bc] c>args "%t%" 1:["a "b] 2:[c"] c>Aaaaaargh! 

Sane parsing seems forever broken.

For your entertainment, try adding miscellaneous ^ , \ , ' , & (&c.) characters to these examples.

You have some great answers above already, but to answer one part of your question:

 set a =b, echo %a %b% c% → bb c% 

What is happening there is that because you have a space before the =, a variable is created called %a% so when you echo %a % that is evaluated correctly as b .

The remaining part b% c% is then evaluated as plain text + an undefined variable % c% , which should be echoed as typed, for me echo %a %b% c% returns bb% c%

I suspect that the ability to include spaces in variable names is more of an oversight than a planned ‘feature’

edit: see accepted answer, what follows is wrong and explains only how to pass a command line to TinyPerl.


Regarding quotes, I have the feeling that the behaviour is the following:

  • when a " is found, string globbing begins
  • when string globbing occurs:
    • every character that is not a " is globbed
    • when a " is found:
      • if it is followed by "" (thus a triple " ) then a double quote is added to the string
      • if it is followed by " (thus a double " ) then a double quote is added to the string and string globbing ends
      • if the next character is not " , string globbing ends
    • when line ends, string globbing ends.

In breve:

"a """ b "" c""" consists of two strings: a " b " and c"

"a"" , "a""" and "a"""" are all the same string if at the end of a line