@@ -1818,13 +1818,20 @@ impl Interpreter {
18181818 /// Returns exit code 0 if result is non-zero, 1 if result is zero
18191819 /// Execute a [[ conditional expression ]]
18201820 async fn execute_conditional ( & mut self , words : & [ Word ] ) -> Result < ExecResult > {
1821- // Expand all words
1822- let mut expanded = Vec :: new ( ) ;
1823- for word in words {
1824- expanded. push ( self . expand_word ( word) . await ?) ;
1821+ // Evaluate with lazy expansion to support short-circuit semantics.
1822+ // In `[[ -n "${X:-}" && "$X" != "off" ]]`, if the left side is false,
1823+ // the right side must NOT be expanded (to avoid set -u errors).
1824+ let result = self . evaluate_conditional_words ( words) . await ;
1825+ // If a nounset error occurred during evaluation, propagate it.
1826+ if let Some ( err_msg) = self . nounset_error . take ( ) {
1827+ self . last_exit_code = 1 ;
1828+ return Ok ( ExecResult {
1829+ stderr : err_msg,
1830+ exit_code : 1 ,
1831+ control_flow : ControlFlow :: Return ( 1 ) ,
1832+ ..Default :: default ( )
1833+ } ) ;
18251834 }
1826-
1827- let result = self . evaluate_conditional ( & expanded) . await ;
18281835 let exit_code = if result { 0 } else { 1 } ;
18291836 self . last_exit_code = exit_code;
18301837
@@ -1837,6 +1844,72 @@ impl Interpreter {
18371844 } )
18381845 }
18391846
1847+ /// Evaluate [[ ]] from raw words with lazy expansion for short-circuit.
1848+ fn evaluate_conditional_words < ' a > (
1849+ & ' a mut self ,
1850+ words : & ' a [ Word ] ,
1851+ ) -> std:: pin:: Pin < Box < dyn std:: future:: Future < Output = bool > + Send + ' a > > {
1852+ Box :: pin ( async move {
1853+ if words. is_empty ( ) {
1854+ return false ;
1855+ }
1856+
1857+ // Helper: get literal text of a word (for operators like &&, ||, !, (, ))
1858+ let word_literal = |w : & Word | -> Option < String > {
1859+ if w. parts . len ( ) == 1
1860+ && let WordPart :: Literal ( s) = & w. parts [ 0 ]
1861+ {
1862+ return Some ( s. clone ( ) ) ;
1863+ }
1864+ None
1865+ } ;
1866+
1867+ // Handle negation
1868+ if word_literal ( & words[ 0 ] ) . as_deref ( ) == Some ( "!" ) {
1869+ return !self . evaluate_conditional_words ( & words[ 1 ..] ) . await ;
1870+ }
1871+
1872+ // Handle parentheses
1873+ if word_literal ( & words[ 0 ] ) . as_deref ( ) == Some ( "(" )
1874+ && word_literal ( & words[ words. len ( ) - 1 ] ) . as_deref ( ) == Some ( ")" )
1875+ {
1876+ return self
1877+ . evaluate_conditional_words ( & words[ 1 ..words. len ( ) - 1 ] )
1878+ . await ;
1879+ }
1880+
1881+ // Look for || (lowest precedence), then && — scan right to left
1882+ for i in ( 0 ..words. len ( ) ) . rev ( ) {
1883+ if word_literal ( & words[ i] ) . as_deref ( ) == Some ( "||" ) && i > 0 {
1884+ let left = self . evaluate_conditional_words ( & words[ ..i] ) . await ;
1885+ if left {
1886+ return true ; // short-circuit: skip right side
1887+ }
1888+ return self . evaluate_conditional_words ( & words[ i + 1 ..] ) . await ;
1889+ }
1890+ }
1891+ for i in ( 0 ..words. len ( ) ) . rev ( ) {
1892+ if word_literal ( & words[ i] ) . as_deref ( ) == Some ( "&&" ) && i > 0 {
1893+ let left = self . evaluate_conditional_words ( & words[ ..i] ) . await ;
1894+ if !left {
1895+ return false ; // short-circuit: skip right side
1896+ }
1897+ return self . evaluate_conditional_words ( & words[ i + 1 ..] ) . await ;
1898+ }
1899+ }
1900+
1901+ // Leaf: expand words and evaluate as a simple condition
1902+ let mut expanded = Vec :: new ( ) ;
1903+ for word in words {
1904+ match self . expand_word ( word) . await {
1905+ Ok ( s) => expanded. push ( s) ,
1906+ Err ( _) => return false ,
1907+ }
1908+ }
1909+ self . evaluate_conditional ( & expanded) . await
1910+ } )
1911+ }
1912+
18401913 /// Evaluate a [[ ]] conditional expression from expanded words.
18411914 fn evaluate_conditional < ' a > (
18421915 & ' a mut self ,
0 commit comments