OILS / test / parse-errors.sh View on Github | oils.pub

857 lines, 417 significant
1#!/usr/bin/env bash
2#
3# Usage:
4# test/parse-errors.sh <function name>
5
6set -o nounset
7set -o pipefail
8set -o errexit
9
10source test/common.sh
11source test/sh-assert.sh # _assert-sh-status
12
13# We can't really run with OSH=bash, because the exit status is often different
14
15# Although it would be nice to IGNORE errors and them some how preview errors.
16
17OSH=${OSH:-bin/osh}
18YSH=${YSH:-bin/ysh}
19
20# More detailed assertions - TODO: remove these?
21
22_assert-status-2() {
23 ### An interface where you can pass flags like -O test-parse_backslash
24
25 local message=$0
26 _assert-sh-status 2 $OSH $message "$@"
27}
28
29_assert-status-2-here() {
30 _assert-status-2 "$@" -c "$(cat)"
31}
32
33_runtime-parse-error() {
34 ### Assert that a parse error happens at runtime, e.g. for [ z z ]
35
36 _osh-error-X 2 "$@"
37}
38
39#
40# Cases
41#
42
43test-syntax-abbrev() {
44 # test frontend/syntax_abbrev.py
45
46 local o1='echo "double $x ${braced}" $(date)'
47 _osh-should-parse "$o1"
48 $OSH --ast-format text -n -c "$o1"
49
50 local o2="echo 'single'"
51 _osh-should-parse "$o2"
52 $OSH --ast-format text -n -c "$o2"
53
54 local y1='var x = i + 42;'
55 _ysh-should-parse "$y1"
56 $YSH --ast-format text -n -c "$y1"
57}
58
59# All in osh/word_parse.py
60test-patsub() {
61 _osh-should-parse 'echo ${x/}'
62 _osh-should-parse 'echo ${x//}'
63
64 _osh-should-parse 'echo ${x/foo}' # pat 'foo', no mode, replace empty
65
66 _osh-should-parse 'echo ${x//foo}' # pat 'foo', replace mode '/', replace empty
67 _osh-should-parse 'echo ${x/%foo}' # same as above
68
69 _osh-should-parse 'echo ${x///foo}'
70
71 _osh-should-parse 'echo ${x///}' # found and fixed bug
72 _osh-should-parse 'echo ${x/%/}' # pat '', replace mode '%', replace ''
73
74 _osh-should-parse 'echo ${x////}' # pat '/', replace mode '/', replace empty
75 _osh-should-parse 'echo ${x/%//}' # pat '', replace mode '%', replace '/'
76
77 # Newline in replacement pattern
78 _osh-should-parse 'echo ${x//foo/replace
79}'
80 _osh-should-parse 'echo ${x//foo/replace$foo}'
81}
82
83test-slice() {
84 _osh-should-parse '${foo:42}'
85 _osh-should-parse '${foo:42+1}'
86
87 # Slicing
88 _osh-parse-error 'echo ${a:1;}'
89 _osh-parse-error 'echo ${a:1:2;}'
90}
91
92# osh/word_parse.py
93test-word-parse() {
94 _osh-parse-error 'echo ${'
95
96 _osh-parse-error 'echo ${a[@Z'
97
98 _osh-parse-error 'echo ${x.}'
99 _osh-parse-error 'echo ${!x.}'
100
101 # I don't seem to be able to tickle errors here
102 #_osh-parse-error 'echo ${a:-}'
103 #_osh-parse-error 'echo ${a#}'
104
105 _osh-parse-error 'echo ${#a.'
106
107 # for (( ))
108 _osh-parse-error 'for (( i = 0; i < 10; i++ ;'
109 # Hm not sure about this
110 _osh-parse-error 'for (( i = 0; i < 10; i++ /'
111
112 _osh-parse-error 'echo @(extglob|foo'
113
114 # Copied from osh/word_parse_test.py. Bugs were found while writing
115 # core/completion_test.py.
116
117 _osh-parse-error '${undef:-'
118 _osh-parse-error '${undef:-$'
119 _osh-parse-error '${undef:-$F'
120
121 _osh-parse-error '${x@'
122 _osh-parse-error '${x@Q'
123
124 _osh-parse-error '${x%'
125
126 _osh-parse-error '${x/'
127 _osh-parse-error '${x/a/'
128 _osh-parse-error '${x/a/b'
129 _osh-parse-error '${x:'
130}
131
132test-dparen() {
133 # (( ))
134
135 _osh-should-parse '(())'
136 _osh-should-parse '(( ))'
137 _osh-parse-error '(( )'
138 _osh-parse-error '(( )x'
139 #_osh-should-parse '$(echo $(( 1 + 2 )) )'
140
141 # Hard case
142 _osh-should-parse '$(echo $(( 1 + 2 )))'
143 _osh-should-parse '$( (()))'
144
145 # More
146 _osh-parse-error '(( 1 + 2 /'
147 _osh-parse-error '(( 1 + 2 )/'
148 _osh-parse-error '(( 1'
149 _osh-parse-error '(('
150}
151
152test-arith-sub() {
153 # $(( ))
154
155 _osh-should-parse 'echo $(( ))'
156 _osh-should-parse 'echo $(())'
157 _osh-parse-error 'echo $(()x'
158
159 _osh-parse-error 'echo $(()'
160
161 _osh-parse-error 'echo $(( 1 + 2 ;'
162 _osh-parse-error 'echo $(( 1 + 2 );'
163 _osh-parse-error 'echo $(( '
164 _osh-parse-error 'echo $(( 1'
165}
166
167
168test-array-literal() {
169 # Array literal with invalid TokenWord.
170 _osh-parse-error 'a=(1 & 2)'
171 _osh-parse-error 'a= (1 2)'
172 _osh-parse-error 'a=(1 2'
173 _osh-parse-error 'a=(1 ${2@} )' # error in word inside array literal
174}
175
176test-arith-context() {
177 # Disable Oil stuff for osh_{parse,eval}.asan
178 if false; then
179 # Non-standard arith sub $[1 + 2]
180 _osh-parse-error 'echo $[ 1 + 2 ;'
181
182 # What's going on here? No location info?
183 _osh-parse-error 'echo $[ 1 + 2 /'
184
185 _osh-parse-error 'echo $[ 1 + 2 / 3'
186 _osh-parse-error 'echo $['
187 fi
188
189 # Should be an error
190 _osh-parse-error 'a[x+]=1'
191
192 # Check what happens when you wrap
193 # This could use more detail - it needs the eval location
194 _osh-error-2 'eval a[x+]=1'
195
196 _osh-parse-error 'a[]=1'
197
198 _osh-parse-error 'a[*]=1'
199
200 # These errors are different because the arithmetic lexer mode has } but not
201 # {. May be changed later.
202 _osh-parse-error '(( a + { ))'
203 _osh-parse-error '(( a + } ))'
204
205}
206
207test-arith-integration() {
208 # Regression: these were not parse errors, but should be!
209 _osh-parse-error 'echo $((a b))'
210 _osh-parse-error '((a b))'
211
212 # Empty arithmetic expressions
213 _osh-should-parse 'for ((x=0; x<5; x++)); do echo $x; done'
214 _osh-should-parse 'for ((; x<5; x++)); do echo $x; done'
215 _osh-should-parse 'for ((; ; x++)); do echo $x; done'
216 _osh-should-parse 'for ((; ;)); do echo $x; done'
217
218 # Extra tokens on the end of each expression
219 _osh-parse-error 'for ((x=0; x<5; x++ b)); do echo $x; done'
220
221 _osh-parse-error 'for ((x=0 b; x<5; x++)); do echo $x; done'
222 _osh-parse-error 'for ((x=0; x<5 b; x++)); do echo $x; done'
223
224 _osh-parse-error '${a:1+2 b}'
225 _osh-parse-error '${a:1+2:3+4 b}'
226
227 _osh-parse-error '${a[1+2 b]}'
228}
229
230test-arith-expr() {
231 # BUG: the token is off here
232 _osh-parse-error '$(( 1 + + ))'
233
234 # BUG: not a great error either
235 _osh-parse-error '$(( 1 2 ))'
236
237 # Triggered a crash!
238 _osh-parse-error '$(( - ; ))'
239
240 # NOTE: This is confusing, should point to ` for command context?
241 _osh-parse-error '$(( ` ))'
242
243 _osh-parse-error '$(( $ ))'
244
245 # Invalid assignments
246 _osh-parse-error '$(( x+1 = 42 ))'
247 _osh-parse-error '$(( (x+42)++ ))'
248 _osh-parse-error '$(( ++(x+42) ))'
249
250 # Note these aren't caught because '1' is an ArithWord like 0x$x
251 #_osh-parse-error '$(( 1 = foo ))'
252 #_osh-parse-error '$(( 1++ ))'
253 #_osh-parse-error '$(( ++1 ))'
254}
255
256test-command-sub() {
257 _osh-parse-error '
258 echo line 2
259 echo $( echo '
260 _osh-parse-error '
261 echo line 2
262 echo ` echo '
263
264 # This is source.Reparsed('backticks', ...)
265
266 # Both unclosed
267 _osh-parse-error '
268 echo line 2
269 echo ` echo \` '
270
271 # Only the inner one is unclosed
272 _osh-parse-error '
273 echo line 2
274 echo ` echo \`unclosed ` '
275
276 _osh-parse-error 'echo `for x in`'
277}
278
279test-bool-expr() {
280 # Extra word
281 _osh-parse-error '[[ a b ]]'
282 _osh-parse-error '[[ a "a"$(echo hi)"b" ]]'
283
284 # Wrong error message
285 _osh-parse-error '[[ a == ]]'
286
287 if false; then
288 # Invalid regex
289 # These are currently only detected at runtime.
290 _osh-parse-error '[[ $var =~ * ]]'
291 _osh-parse-error '[[ $var =~ + ]]'
292 fi
293
294 # Unbalanced parens
295 _osh-parse-error '[[ ( 1 == 2 - ]]'
296
297 _osh-parse-error '[[ == ]]'
298 _osh-parse-error '[[ ) ]]'
299 _osh-parse-error '[[ ( ]]'
300
301 _osh-parse-error '[[ ;;; ]]'
302 _osh-parse-error '[['
303
304 # Expected right )
305 _osh-parse-error '[[ ( a == b foo${var} ]]'
306}
307
308test-regex-nix() {
309 ### Based on Nix bug
310
311 # Nix idiom - added space
312 _osh-should-parse '
313if [[ ! (" ${params[*]} " =~ " -shared " || " ${params[*]} " =~ " -static " ) ]]; then
314 echo hi
315fi
316'
317
318 # (x) is part of the regex
319 _osh-should-parse '
320if [[ (foo =~ (x) ) ]]; then
321 echo hi
322fi
323'
324 # Nix idiom - reduced
325 _osh-should-parse '
326if [[ (foo =~ x) ]]; then
327 echo hi
328fi
329'
330
331 # Nix idiom - original
332 _osh-should-parse '
333if [[ ! (" ${params[*]} " =~ " -shared " || " ${params[*]} " =~ " -static ") ]]; then
334 echo hi
335fi
336'
337}
338
339test-regex-pipe() {
340 # Pipe in outer expression - it becomes Lit_Other, which is fine
341
342 # Well we need a special rule for this probably
343 local s='[[ a =~ b|c ]]'
344 bash -n -c "$s"
345 _osh-should-parse "$s"
346}
347
348test-regex-space() {
349 # initial space
350 _osh-should-parse '[[ a =~ ( ) ]]'
351 _osh-should-parse '[[ a =~ (b c) ]]'
352 _osh-should-parse '[[ a =~ (a b)(c d) ]]'
353
354 # Hm bash allows newline inside (), but not outside
355 # I feel like we don't need to duplicate this
356
357 local s='[[ a =~ (b
358c) ]]'
359 bash -n -c "$s"
360 echo bash=$?
361
362 _osh-should-parse "$s"
363}
364
365test-regex-right-paren() {
366 # BashRegex lexer mode
367 _osh-should-parse '[[ a =~ b ]]'
368 _osh-should-parse '[[ a =~ (b) ]]' # this is a regex
369 _osh-should-parse '[[ (a =~ b) ]]' # this is grouping
370 _osh-should-parse '[[ (a =~ (b)) ]]' # regex and grouping!
371
372 _osh-parse-error '[[ (a =~ (' # EOF
373 _osh-parse-error '[[ (a =~ (b' # EOF
374
375 return
376 # Similar thing for extglob
377 _osh-should-parse '[[ a == b ]]'
378 _osh-should-parse '[[ a == @(b) ]]' # this is a regex
379 _osh-should-parse '[[ (a == b) ]]' # this is grouping
380 _osh-should-parse '[[ (a == @(b)) ]]' # regex and grouping!
381}
382
383
384# These don't have any location information.
385test-test-builtin() {
386 # Some of these come from osh/bool_parse.py, and some from
387 # osh/builtin_bracket.py.
388
389 # Extra token
390 _runtime-parse-error '[ x -a y f ]'
391 _runtime-parse-error 'test x -a y f'
392
393 # Missing closing ]
394 _runtime-parse-error '[ x '
395
396 # Hm some of these errors are wonky. Need positions.
397 _runtime-parse-error '[ x x ]'
398
399 _runtime-parse-error '[ x x "a b" ]'
400
401 # This is a runtime error but is handled similarly
402 _runtime-parse-error '[ -t xxx ]'
403
404 _runtime-parse-error '[ \( x -a -y -a z ]'
405
406 # -o tests if an option is enabled.
407 #_osh-parse-error '[ -o x ]'
408}
409
410test-printf-builtin() {
411 _runtime-parse-error 'printf %'
412 _runtime-parse-error 'printf [%Z]'
413
414 _runtime-parse-error 'printf -v "-invalid-" %s foo'
415}
416
417test-other-builtins() {
418 _runtime-parse-error 'shift 1 2'
419 _runtime-parse-error 'shift zzz'
420
421 _runtime-parse-error 'pushd x y'
422 _runtime-parse-error 'pwd -x'
423
424 _runtime-parse-error 'pp x foo a-x'
425
426 _runtime-parse-error 'wait zzz'
427 _runtime-parse-error 'wait %jobspec-not-supported'
428
429 _runtime-parse-error 'unset invalid-var-name'
430 _runtime-parse-error 'getopts 'hc:' invalid-var-name'
431}
432
433test-quoted-strings() {
434 _osh-parse-error '"unterminated double'
435
436 _osh-parse-error "'unterminated single"
437
438 _osh-parse-error '
439 "unterminated double multiline
440 line 1
441 line 2'
442
443 _osh-parse-error "
444 'unterminated single multiline
445 line 1
446 line 2"
447}
448
449test-braced-var-sub() {
450 # These should have ! for a prefix query
451 _osh-parse-error 'echo ${x*}'
452 _osh-parse-error 'echo ${x@}'
453
454 _osh-parse-error 'echo ${x.}'
455}
456
457test-cmd-parse() {
458 _osh-parse-error 'FOO=1 break'
459 _osh-parse-error 'break 1 2'
460
461 _osh-parse-error 'x"y"() { echo hi; }'
462
463 _osh-parse-error 'function x"y" { echo hi; }'
464
465 _osh-parse-error '}'
466
467 _osh-parse-error 'case foo in *) echo '
468 _osh-parse-error 'case foo in x|) echo '
469
470 _osh-parse-error 'ls foo|'
471 _osh-parse-error 'ls foo&&'
472
473 _osh-parse-error 'foo()'
474
475 # parse_ignored
476 _osh-should-parse 'break >out'
477 _ysh-parse-error 'break >out'
478
479 # Unquoted (
480 _osh-parse-error '[ ( x ]'
481}
482
483test-append() {
484 # from spec/test-append.test.sh. bash treats this as a runtime error, but it's a
485 # parse error in OSH.
486 _osh-parse-error 'a[-1]+=(4 5)'
487}
488
489test-redirect() {
490 _osh-parse-error 'echo < <<'
491 _osh-parse-error 'echo $( echo > >> )'
492}
493
494test-simple-command() {
495 _osh-parse-error 'PYTHONPATH=. FOO=(1 2) python'
496 # not statically detected after dynamic assignment
497 #_osh-parse-error 'echo foo FOO=(1 2)'
498
499 _osh-parse-error 'PYTHONPATH+=1 python'
500}
501
502test-leading-equals() {
503 # allowed in OSH for compatibility
504 _osh-should-parse '=var'
505 _osh-should-parse '=a[i]'
506
507 # In YSH, avoid confusion with = var and = f(x)
508 _ysh-parse-error '=var'
509 _ysh-parse-error '=a[i]'
510}
511
512# Old code? All these pass
513DISABLED-assign() {
514 _osh-parse-error 'local name$x'
515 _osh-parse-error 'local "ab"'
516 _osh-parse-error 'local a.b'
517
518 _osh-parse-error 'FOO=1 local foo=1'
519}
520
521# I can't think of any other here doc error conditions except arith/var/command
522# substitution, and unterminated.
523test-here-doc() {
524 # Arith in here doc
525 _osh-parse-error 'cat <<EOF
526$(( 1 * ))
527EOF
528'
529
530 # Varsub in here doc
531 _osh-parse-error 'cat <<EOF
532invalid: ${a!}
533EOF
534'
535
536 _osh-parse-error 'cat <<EOF
537$(for x in )
538EOF
539'
540}
541
542test-here-doc-delimiter() {
543 # NOTE: This is more like the case where.
544 _osh-parse-error 'cat << $(invalid here end)'
545
546 # TODO: Arith parser doesn't have location information
547 _osh-parse-error 'cat << $((1+2))'
548 _osh-parse-error 'cat << a=(1 2 3)'
549 _osh-parse-error 'cat << \a$(invalid)'
550
551 # Actually the $invalid part should be highlighted... yeah an individual
552 # part is the problem.
553 #"cat << 'single'$(invalid)"
554 _osh-parse-error 'cat << "double"$(invalid)'
555 _osh-parse-error 'cat << ~foo/$(invalid)'
556 _osh-parse-error 'cat << $var/$(invalid)'
557}
558
559test-args-parse-builtin() {
560 _runtime-parse-error 'read -x' # invalid
561 _runtime-parse-error 'builtin read -x' # ditto
562
563 _runtime-parse-error 'read -n' # expected argument for -n
564 _runtime-parse-error 'read -n x' # expected integer
565
566 _runtime-parse-error 'set -o errexit +o oops'
567
568 # not implemented yet
569 #_osh-parse-error 'read -t x' # expected floating point number
570
571 # TODO:
572 # - invalid choice
573 # - Oil flags: invalid long flag, boolean argument, etc.
574}
575
576test-args-parse-more() {
577 _runtime-parse-error 'set -z'
578 _runtime-parse-error 'shopt -s foo'
579 _runtime-parse-error 'shopt -z'
580}
581
582DISABLED-args-parse-main() {
583 $OSH --ast-format x
584
585 $OSH -o errexit +o oops
586}
587
588test-invalid-brace-ranges() {
589 _osh-parse-error 'echo {1..3..-1}'
590 _osh-parse-error 'echo {1..3..0}'
591 _osh-parse-error 'echo {3..1..1}'
592 _osh-parse-error 'echo {3..1..0}'
593 _osh-parse-error 'echo {a..Z}'
594 _osh-parse-error 'echo {a..z..0}'
595 _osh-parse-error 'echo {a..z..-1}'
596 _osh-parse-error 'echo {z..a..1}'
597}
598
599test-extra-newlines() {
600 _osh-parse-error '
601 for
602 do
603 done
604 '
605
606 _osh-parse-error '
607 case
608 in esac
609 '
610
611 _osh-parse-error '
612 while
613 do
614 done
615 '
616
617 _osh-parse-error '
618 if
619 then
620 fi
621 '
622
623 _osh-parse-error '
624 if true
625 then
626 elif
627 then
628 fi
629 '
630
631 _osh-parse-error '
632 case |
633 in
634 esac
635 '
636
637 _osh-parse-error '
638 case ;
639 in
640 esac
641 '
642
643 _osh-should-parse '
644 if
645 true
646 then
647 fi
648 '
649
650 _osh-should-parse '
651 while
652 false
653 do
654 done
655 '
656
657 _osh-should-parse '
658 while
659 true;
660 false
661 do
662 done
663 '
664
665 _osh-should-parse '
666 if true
667 then
668 fi
669 '
670
671 _osh-should-parse '
672 while true;
673 false
674 do
675 done
676 '
677}
678
679test-parse_backticks() {
680
681 # These are allowed
682 _osh-should-parse 'echo `echo hi`'
683 _osh-should-parse 'echo "foo = `echo hi`"'
684
685 _assert-status-2 +O test-parse_backticks -n -c 'echo `echo hi`'
686 _assert-status-2 +O test-parse_backticks -n -c 'echo "foo = `echo hi`"'
687}
688
689test-shell_for() {
690
691 _osh-parse-error 'for x in &'
692
693 _osh-parse-error 'for (( i=0; i<10; i++ )) ls'
694
695 # ( is invalid
696 _osh-parse-error 'for ( i=0; i<10; i++ )'
697
698 _osh-parse-error 'for $x in 1 2 3; do echo $i; done'
699 _osh-parse-error 'for x.y in 1 2 3; do echo $i; done'
700 _osh-parse-error 'for x in 1 2 3; &'
701 _osh-parse-error 'for foo BAD'
702
703 # BUG fix: var is a valid name
704 _osh-should-parse 'for var in x; do echo $var; done'
705}
706
707#
708# Different source_t variants
709#
710
711test-nested_source_argvword() {
712 # source.ArgvWord
713 _runtime-parse-error '
714 code="printf % x"
715 eval $code
716 '
717}
718
719test-eval_parse_error() {
720 _runtime-parse-error '
721 x="echo )"
722 eval $x
723 '
724}
725
726trap_parse_error() {
727 _runtime-parse-error '
728 trap "echo )" EXIT
729 '
730}
731
732test-proc-func-reserved() {
733 ### Prevents confusion
734
735 _osh-parse-error 'proc p (x) { echo hi }'
736 _osh-parse-error 'func f (x) { return (x) }'
737
738 # In expression mode, reserved for
739 # var x = proc (x; y) ^( echo hi )
740 _osh-parse-error '= func'
741 _osh-parse-error '= proc'
742}
743
744# Cases in their own file
745cases-in-files() {
746 for test_file in test/parse-errors/*.sh; do
747 case-banner "FILE $test_file"
748
749 set +o errexit
750 $OSH $test_file
751 local status=$?
752 set -o errexit
753
754 if test -z "${SH_ASSERT_DISABLE:-}"; then
755 if test $status != 2; then
756 die "Expected status 2 from parse error file, got $status"
757 fi
758 fi
759 done
760}
761
762test-case() {
763 readonly -a YES=(
764 # Right is optional
765 'case $x in foo) echo
766esac'
767 'case $x in foo) echo ;; esac'
768 'case $x in foo) echo ;& esac'
769 'case $x in foo) echo ;;& esac'
770 )
771
772 readonly -a NO=(
773 ';&'
774 'echo ;&'
775 'echo ;;&'
776 )
777
778 for c in "${YES[@]}"; do
779 echo "--- test-case YES $c"
780
781 _osh-should-parse "$c"
782 echo
783
784 bash -n -c "$c"
785 echo bash=$?
786 done
787
788 for c in "${NO[@]}"; do
789 echo "--- test-case NO $c"
790
791 _osh-parse-error "$c"
792
793 set +o errexit
794 bash -n -c "$c"
795 echo bash=$?
796 set -o errexit
797 done
798}
799
800all() {
801 section-banner 'Cases in Files'
802
803 cases-in-files
804
805 section-banner 'Cases in Functions, with strings'
806
807 run-test-funcs
808}
809
810# TODO: Something like test/parse-err-compare.sh
811
812all-with-bash() {
813 # override OSH and YSH
814 SH_ASSERT_DISABLE=1 OSH=bash YSH=bash all
815}
816
817all-with-dash() {
818 # override OSH and YSH
819 SH_ASSERT_DISABLE=1 OSH=dash YSH=dash all
820}
821
822soil-run-py() {
823 ### Run in CI with Python
824
825 # output _tmp/other/parse-errors.txt
826
827 all
828}
829
830soil-run-cpp() {
831 ninja _bin/cxx-asan/osh
832 OSH=_bin/cxx-asan/osh all
833}
834
835release-oils-for-unix() {
836 readonly OIL_VERSION=$(head -n 1 oils-version.txt)
837 local dir="../benchmark-data/src/oils-for-unix-$OIL_VERSION"
838
839 # Maybe rebuild it
840 pushd $dir
841 _build/oils.sh --skip-rebuild
842 popd
843
844 local suite_name=parse-errors-osh-cpp
845 OSH=$dir/_bin/cxx-opt-sh/osh \
846 run-other-suite-for-release $suite_name all
847}
848
849run-for-release() {
850 ### Test with bin/osh and the ASAN binary.
851
852 run-other-suite-for-release parse-errors all
853
854 release-oils-for-unix
855}
856
857"$@"