1 # spec/ysh-methods
2
3 ## our_shell: ysh
4 ## oils_failures_allowed: 2
5
6 #### => operator for pure computation is allowed (may be mandatory later)
7
8 # later we may make it mandatory
9
10 if ("abc" => startsWith("a")) {
11 echo yes
12 }
13
14 var mylist = [1, 2, 3]
15
16 # This one should be ->
17 call mylist->pop()
18 echo 'ok'
19
20 ## STDOUT:
21 yes
22 ok
23 ## END
24
25 #### => can be used to chain free functions
26
27 func dictfunc() {
28 return ({k1: 'spam', k2: 'eggs'})
29 }
30
31 echo $[list(dictfunc()) => join('/') => upper()]
32
33 # This is nicer and more consistent
34 echo $[dictfunc() => list() => join('/') => upper()]
35
36 ## STDOUT:
37 K1/K2
38 K1/K2
39 ## END
40
41 #### Str => startsWith(Str) and endsWith(Str), simple
42 func test(s, p) { echo $[s => startsWith(p)] $[s => endsWith(p)] }
43
44 call test('', '')
45 call test('abc', '')
46 call test('abc', 'a')
47 call test('abc', 'b')
48 call test('abc', 'c')
49 call test('abc', 'z')
50 call test('', 'abc')
51 ## status: 0
52 ## STDOUT:
53 true true
54 true true
55 true false
56 false false
57 false true
58 false false
59 false false
60 ## END
61
62 #### Str => startsWith(Str) and endsWith(Str), matches bytes not runes
63 func test(s, p) { echo $[s => startsWith(p)] $[s => endsWith(p)] }
64
65 call test(b'\yce\ya3', u'\u{03a3}')
66 call test(b'\yce\ya3', b'\yce')
67 call test(b'\yce\ya3', b'\ya3')
68 call test(b'\yce', b'\yce')
69 ## status: 0
70 ## STDOUT:
71 true true
72 true false
73 false true
74 true true
75 ## END
76
77 #### Str => startsWith(Str) and endsWith(Str), eggex
78 func test(s, p) { echo $[s => startsWith(p)] $[s => endsWith(p)] }
79
80 call test('abc', / d+ /)
81 call test('abc', / [ a b c ] /)
82 call test('abc', / 'abc' /)
83 call test('cba', / d+ /)
84 call test('cba', / [ a b c ] /)
85 call test('cba', / 'abc' /)
86 ## status: 0
87 ## STDOUT:
88 false false
89 true true
90 true true
91 false false
92 true true
93 false false
94 ## END
95
96 #### Str => startsWith(Str) and endsWith(Str), eggex with anchors
97 func test(s, p) { echo $[s => startsWith(p)] $[s => endsWith(p)] }
98
99 call test('ab', / %start 'a' /)
100 call test('ab', / 'a' %end /)
101 call test('ab', / %start 'a' %end /)
102 call test('ab', / %start 'b' /)
103 call test('ab', / 'b' %end /)
104 call test('ab', / %start 'b' %end /)
105 ## status: 0
106 ## STDOUT:
107 true false
108 false false
109 false false
110 false false
111 false true
112 false false
113 ## END
114
115 #### Str => startsWith(Str) and endsWith(Str), eggex matches bytes not runes
116 func test(s, p) { echo $[s => startsWith(p)] $[s => endsWith(p)] }
117
118 call test(u'\u{03a3}', / dot /)
119 call test(u'\u{03a3}', / ![z] /)
120 call test(b'\yce', / dot /) # Fails: eggex does not match bytes
121 call test(b'\yce', / ![z] /) # Fails: eggex does not match bytes
122 ## status: 0
123 ## STDOUT:
124 true true
125 true true
126 true true
127 true true
128 ## END
129
130 #### Str => startsWith(), no args
131 = 'abc' => startsWith()
132 ## status: 3
133
134 #### Str => startsWith(), too many args
135 = 'abc' => startsWith('extra', 'arg')
136 ## status: 3
137
138 #### Str => endsWith(), no args
139 = 'abc' => endsWith()
140 ## status: 3
141
142 #### Str => endsWith(), too many args
143 = 'abc' => endsWith('extra', 'arg')
144 ## status: 3
145
146 #### Str => trim*() with no args trims whitespace
147 func test(s) { write --sep ', ' --j8 $[s => trimStart()] $[s => trimEnd()] $[s => trim()] }
148
149 call test("")
150 call test(" ")
151 call test("mystr")
152 call test(" mystr")
153 call test("mystr ")
154 call test(" mystr ")
155 call test(" my str ")
156 ## status: 0
157 ## STDOUT:
158 "", "", ""
159 "", "", ""
160 "mystr", "mystr", "mystr"
161 "mystr", " mystr", "mystr"
162 "mystr ", "mystr", "mystr"
163 "mystr ", " mystr", "mystr"
164 "my str ", " my str", "my str"
165 ## END
166
167 #### Str => trim*() with a simple string pattern trims pattern
168 func test(s, p) { write --sep ', ' --j8 $[s => trimStart(p)] $[s => trimEnd(p)] $[s => trim(p)] }
169
170 call test('' , 'xyz')
171 call test(' ' , 'xyz')
172 call test('xy' , 'xyz')
173 call test('yz' , 'xyz')
174 call test('xyz' , 'xyz')
175 call test('xyzxyz' , 'xyz')
176 call test('xyzxyzxyz', 'xyz')
177 ## status: 0
178 ## STDOUT:
179 "", "", ""
180 " ", " ", " "
181 "xy", "xy", "xy"
182 "yz", "yz", "yz"
183 "", "", ""
184 "xyz", "xyz", ""
185 "xyzxyz", "xyzxyz", "xyz"
186 ## END
187
188 #### Str => trim*() with a string pattern trims bytes not runes
189 func test(s, p) { write --sep ', ' --j8 $[s => trimStart(p)] $[s => trimEnd(p)] $[s => trim(p)] }
190
191 call test(b'\yce\ya3', u'\u{03a3}')
192 call test(b'\yce\ya3', b'\yce')
193 call test(b'\yce\ya3', b'\ya3')
194 ## status: 0
195 ## STDOUT:
196 "", "", ""
197 b'\ya3', "Σ", b'\ya3'
198 "Σ", b'\yce', b'\yce'
199 ## END
200
201 #### Str => trim*() with an eggex pattern trims pattern
202 func test(s, p) { write --sep ', ' --j8 $[s => trimStart(p)] $[s => trimEnd(p)] $[s => trim(p)] }
203
204 call test('' , / 'xyz' /)
205 call test(' ' , / 'xyz' /)
206 call test('xy' , / 'xyz' /)
207 call test('yz' , / 'xyz' /)
208 call test('xyz' , / 'xyz' /)
209 call test('xyzxyz' , / 'xyz' /)
210 call test('xyzxyzxyz', / 'xyz' /)
211 call test('xyzabcxyz', / 'xyz' /)
212 call test('xyzabcxyz', / %start 'xyz' /)
213 call test('xyzabcxyz', / 'xyz' %end /)
214 call test('123abc123', / d+ /)
215 ## status: 0
216 ## STDOUT:
217 "", "", ""
218 " ", " ", " "
219 "xy", "xy", "xy"
220 "yz", "yz", "yz"
221 "", "", ""
222 "xyz", "xyz", ""
223 "xyzxyz", "xyzxyz", "xyz"
224 "abcxyz", "xyzabc", "abc"
225 "abcxyz", "xyzabcxyz", "abcxyz"
226 "xyzabcxyz", "xyzabc", "xyzabc"
227 "abc123", "123abc", "abc"
228 ## END
229
230 #### Str => trim*() with an eggex pattern trims bytes not runes
231 func test(s, p) { write --sep ', ' --j8 $[s => trimStart(p)] $[s => trimEnd(p)] $[s => trim(p)] }
232
233 call test(u'\u{03a3}', / dot /) # Fails: eggex does not match bytes, so entire rune is trimmed.
234 call test(u'\u{03a3}', / ![z] /) # Fails: eggex does not match bytes, so entire rune is trimmed.
235 call test(b'\yce', / dot /) # Fails: eggex does not match bytes, so nothing is trimmed.
236 call test(b'\yce', / ![z] /) # Fails: eggex does not match bytes, so nothing is trimmed.
237 ## status: 0
238 ## STDOUT:
239 b'\ya3', b'\yce', ""
240 b'\ya3', b'\yce', ""
241 "", "", ""
242 "", "", ""
243 ## END
244
245 #### Str => trim(), too many args
246 = 'mystr' => trim('extra', 'args')
247 ## status: 3
248
249 #### Str => trimStart(), too many args
250 = 'mystr' => trimStart('extra', 'args')
251 ## status: 3
252
253 #### Str => trimEnd(), too many args
254 = 'mystr' => trimEnd('extra', 'args')
255 ## status: 3
256
257 #### Str => trim(), unicode whitespace aware
258
259 # Supported set of whitespace characters. The full set of Unicode whitespace
260 # characters is not supported. See comments in the implementation.
261 var spaces = [
262 b'\u{0009}', # Horizontal tab (\t)
263 b'\u{000A}', # Newline (\n)
264 b'\u{000B}', # Vertical tab (\v)
265 b'\u{000C}', # Form feed (\f)
266 b'\u{000D}', # Carriage return (\r)
267 b'\u{0020}', # Normal space
268 b'\u{00A0}', # No-break space <NBSP>
269 b'\u{FEFF}', # Zero-width no-break space <ZWNBSP>
270 ] => join('')
271
272 echo $["$spaces YSH $spaces" => trim()]
273 ## status: 0
274 ## STDOUT:
275 YSH
276 ## END
277
278 #### Str => trim*(), unicode decoding errors
279 var badUtf = b'\yF9'
280
281 echo trim
282
283 # We only decode UTF until the first non-space char. So the bad UTF-8 is
284 # missed.
285 try { call " a$[badUtf]b " => trim() }
286 echo status=$_status
287
288 # These require trim to decode the badUtf, so an error is raised
289 try { call "$[badUtf]b " => trim() }
290 echo status=$_status
291 try { call " a$[badUtf]" => trim() }
292 echo status=$_status
293
294 # Similarly, trim{Left,Right} will assume correct encoding until shown
295 # otherwise.
296 echo trimStart
297 try { call " a$[badUtf]" => trimStart() }
298 echo status=$_status
299 try { call "$[badUtf]b " => trimStart() }
300 echo status=$_status
301
302 echo trimEnd
303 try { call "$[badUtf]b " => trimEnd() }
304 echo status=$_status
305 try { call " a$[badUtf]" => trimEnd() }
306 echo status=$_status
307
308 ## STDOUT:
309 trim
310 status=0
311 status=3
312 status=3
313 trimStart
314 status=0
315 status=3
316 trimEnd
317 status=0
318 status=3
319 ## END
320
321 #### Str => trimStart(), unicode decoding error types
322 var badStrs = [
323 b'\yF4\yA2\yA4\yB0', # Too large of a codepoint
324 b'\yED\yBF\y80', # Surrogate
325 b'\yC1\y81', # Overlong
326 b'\y80', b'\yFF', # Does not match UTF8 bit pattern
327 ]
328
329 for badStr in (badStrs) {
330 try { call badStr => trimStart() }
331 echo status=$_status
332 }
333
334 ## STDOUT:
335 status=3
336 status=3
337 status=3
338 status=3
339 status=3
340 ## END
341
342 #### Str => trimEnd(), unicode decoding error types
343 # Tests the backwards UTF-8 decoder
344 var badStrs = [
345 b'\yF4\yA2\yA4\yB0', # Too large of a codepoint
346 b'\yED\yBF\y80', # Surrogate
347 b'\yC1\y81', # Overlong
348 b'\y80', b'\yFF', # Does not match UTF8 bit pattern
349 ]
350
351 for badStr in (badStrs) {
352 try { call badStr => trimEnd() }
353 echo status=$_status
354 }
355
356 ## STDOUT:
357 status=3
358 status=3
359 status=3
360 status=3
361 status=3
362 ## END
363
364 #### Str => trim*(), zero-codepoints are not NUL-terminators
365 json write (b' \y00 ' => trim())
366 json write (b' \y00 ' => trimStart())
367 json write (b' \y00 ' => trimEnd())
368 ## STDOUT:
369 "\u0000"
370 "\u0000 "
371 " \u0000"
372 ## END
373
374 #### Str => split(sep), non-empty str sep
375 pp test_ ('a,b,c'.split(','))
376 pp test_ ('aa'.split('a'))
377 pp test_ ('a<>b<>c<d'.split('<>'))
378 pp test_ ('a;b;;c'.split(';'))
379 pp test_ (''.split('foo'))
380 ## STDOUT:
381 (List) ["a","b","c"]
382 (List) ["","",""]
383 (List) ["a","b","c<d"]
384 (List) ["a","b","","c"]
385 (List) []
386 ## END
387
388 #### Str => split(sep), eggex sep
389 pp test_ ('a,b;c'.split(/ ',' | ';' /))
390 pp test_ ('aa'.split(/ dot /))
391 pp test_ ('a<>b@@c<d'.split(/ '<>' | '@@' /))
392 pp test_ ('a b cd'.split(/ space+ /))
393 pp test_ (''.split(/ dot /))
394 ## STDOUT:
395 (List) ["a","b","c"]
396 (List) ["","",""]
397 (List) ["a","b","c<d"]
398 (List) ["a","b","cd"]
399 (List) []
400 ## END
401
402 #### Str => split(sep, count), non-empty str sep
403 pp test_ ('a,b,c'.split(',', count=-1))
404 pp test_ ('a,b,c'.split(',', count=-2)) # Any negative count means "ignore count"
405 pp test_ ('aa'.split('a', count=1))
406 pp test_ ('a<>b<>c<d'.split('<>', count=10))
407 pp test_ ('a;b;;c'.split(';', count=2))
408 pp test_ (''.split('foo', count=3))
409 pp test_ ('a,b,c'.split(',', count=0))
410 pp test_ (''.split(',', count=0))
411 ## STDOUT:
412 (List) ["a","b","c"]
413 (List) ["a","b","c"]
414 (List) ["","a"]
415 (List) ["a","b","c<d"]
416 (List) ["a","b",";c"]
417 (List) []
418 (List) ["a,b,c"]
419 (List) []
420 ## END
421
422 #### Str => split(sep, count), eggex sep
423 pp test_ ('a,b;c'.split(/ ',' | ';' /, count=-1))
424 pp test_ ('aa'.split(/ dot /, count=1))
425 pp test_ ('a<>b@@c<d'.split(/ '<>' | '@@' /, count=50))
426 pp test_ ('a b c'.split(/ space+ /, count=0))
427 pp test_ (''.split(/ dot /, count=1))
428 ## STDOUT:
429 (List) ["a","b","c"]
430 (List) ["","a"]
431 (List) ["a","b","c<d"]
432 (List) ["a b c"]
433 (List) []
434 ## END
435
436 #### Str => split(), usage errors
437 try { pp test_ ('abc'.split('')) } # Sep cannot be ""
438 echo status=$[_error.code]
439 try { pp test_ ('abc'.split()) } # Sep must be present
440 echo status=$[_error.code]
441 try { pp test_ (b'\y00a\y01'.split(/ 'a' /)) } # Cannot split by eggex when str has NUL-byte
442 echo status=$[_error.code]
443 try { pp test_ (b'abc'.split(/ space* /)) } # Eggex cannot accept empty string
444 echo status=$[_error.code]
445 try { pp test_ (b'abc'.split(/ dot* /)) } # But in some cases the input doesn't cause an
446 # infinite loop, so we actually allow it!
447 echo status=$[_error.code]
448 ## STDOUT:
449 status=3
450 status=3
451 status=3
452 status=3
453 (List) ["",""]
454 status=0
455 ## END
456
457 #### Str => split(), non-ascii
458 pp test_ ('🌞🌝🌞🌝🌞'.split('🌝'))
459 pp test_ ('🌞🌝🌞🌝🌞'.split(/ '🌝' /))
460 ## STDOUT:
461 (List) ["🌞","🌞","🌞"]
462 (List) ["🌞","🌞","🌞"]
463 ## END
464
465 #### Str.lines()
466
467 var empty = u''
468 pp test_ (empty.lines())
469
470 var one = u'\n'
471 pp test_ (one.lines())
472
473 var two = u'\n\n'
474 pp test_ (two.lines())
475
476 var three = u'one\ntwo\nthree' # no trailing newline
477 pp test_ (three.lines())
478
479 var foo = u'foo' # no trailing newline
480 pp test_ (foo.lines())
481
482 echo
483
484 var s = u'foo\nbar\n'
485 pp test_ (s.lines())
486
487 var z = b'foo\y00bar\y00'
488 pp test_ (z.lines(eol=\y00))
489
490 var r = u'foo\r\nbar\r\n'
491 pp test_ (r.lines(eol=u'\r\n'))
492
493 echo
494
495 pp test_ (empty.lines(eol=\y00))
496 pp test_ (z.lines())
497 pp test_ (r.lines())
498
499 ## STDOUT:
500 (List) []
501 (List) [""]
502 (List) ["",""]
503 (List) ["one","two","three"]
504 (List) ["foo"]
505
506 (List) ["foo","bar"]
507 (List) ["foo","bar"]
508 (List) ["foo","bar"]
509
510 (List) []
511 (List) ["foo\u0000bar\u0000"]
512 (List) ["foo\r","bar\r"]
513 ## END
514
515 #### read-lines can be built on top of read --all and Str.lines()
516
517 proc read-lines (; out) {
518 read --all
519 call out->setValue(_reply.lines())
520 }
521
522 seq 4 7 > tmp.txt
523
524 read-lines (&x) < tmp.txt
525
526 pp test_ (x)
527
528 ## STDOUT:
529 (List) ["4","5","6","7"]
530 ## END
531
532 #### Dict => values()
533 var en2fr = {}
534 setvar en2fr["hello"] = "bonjour"
535 setvar en2fr["friend"] = "ami"
536 setvar en2fr["cat"] = "chat"
537 pp test_ (en2fr => values())
538 ## status: 0
539 ## STDOUT:
540 (List) ["bonjour","ami","chat"]
541 ## END
542
543 #### Dict -> erase()
544 var book = {title: "The Histories", author: "Herodotus"}
545 call book->erase("author")
546 pp test_ (book)
547 # confirm method is idempotent
548 call book->erase("author")
549 pp test_ (book)
550 ## status: 0
551 ## STDOUT:
552 (Dict) {"title":"The Histories"}
553 (Dict) {"title":"The Histories"}
554 ## END
555
556 #### Dict -> get()
557 var book = {title: "Hitchhiker's Guide", published: 1979}
558 pp test_ (book => get("title", ""))
559 pp test_ (book => get("published", 0))
560 pp test_ (book => get("author", ""))
561 ## status: 0
562 ## STDOUT:
563 (Str) "Hitchhiker's Guide"
564 (Int) 1979
565 (Str) ""
566 ## END
567
568 #### Separation of -> attr and () calling
569 const check = "abc" => startsWith
570 pp test_ (check("a"))
571 ## status: 0
572 ## STDOUT:
573 (Bool) true
574 ## END
575
576 #### Bound methods, receiver value/reference semantics
577 var is_a_ref = { "foo": "bar" }
578 const f = is_a_ref => keys
579 pp test_ (f())
580 setvar is_a_ref["baz"] = 42
581 pp test_ (f())
582
583 var is_a_val = "abc"
584 const g = is_a_val => startsWith
585 pp test_ (g("a"))
586 setvar is_a_val = "xyz"
587 pp test_ (g("a"))
588 ## status: 0
589 ## STDOUT:
590 (List) ["foo"]
591 (List) ["foo","baz"]
592 (Bool) true
593 (Bool) true
594 ## END
595
596 #### List->clear()
597 var empty = []
598 var items = [1, 2, 3]
599
600 call empty->clear()
601 call items->clear()
602
603 pp test_ (empty)
604 pp test_ (items)
605
606 ## STDOUT:
607 (List) []
608 (List) []
609 ## END
610
611 #### List => indexOf()
612 var items = [1, '2', 3, { 'a': 5 }]
613
614 json write (items => indexOf('a'))
615 json write (items => indexOf(1))
616 json write (items => indexOf('2'))
617 json write (items => indexOf({'a': 5}))
618 ## STDOUT:
619 -1
620 0
621 1
622 3
623 ## END
624
625 #### List => lastIndexOf()
626 var items = ['a', 'b', 'c', 'b', 'a']
627
628 json write (items => lastIndexOf('a'))
629 json write (items => lastIndexOf('b'))
630 json write (items => lastIndexOf('c'))
631 json write (items => lastIndexOf('d'))
632 ## STDOUT:
633 4
634 3
635 2
636 -1
637 ## END
638
639 #### List => join()
640 var items = [1, 2, 3]
641
642 json write (items => join()) # default separator is ''
643 json write (items => join(" ")) # explicit separator (can be any number or chars)
644 json write (items => join(", ")) # separator can be any number of chars
645
646 try {
647 json write (items => join(1)) # separator must be a string
648 }
649 echo "failed with status $_status"
650 ## STDOUT:
651 "123"
652 "1 2 3"
653 "1, 2, 3"
654 failed with status 3
655 ## END
656
657 #### List->reverse()
658
659 var empty = []
660
661 var a = [0]
662 var b = [2, 1, 3]
663 var c = :| hello world |
664
665 call empty->reverse()
666 call a->reverse()
667 call b->reverse()
668 call c->reverse()
669
670 pp test_ (empty)
671 pp test_ (a)
672 pp test_ (b)
673 pp test_ (c)
674
675 ## STDOUT:
676 (List) []
677 (List) [0]
678 (List) [3,1,2]
679 (List) ["world","hello"]
680 ## END
681
682 #### List->reverse() from iterator
683 var x = list(0 ..< 3)
684 call x->reverse()
685 write @x
686 ## STDOUT:
687 2
688 1
689 0
690 ## END
691