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 | #### Dict => values()
|
466 | var en2fr = {}
|
467 | setvar en2fr["hello"] = "bonjour"
|
468 | setvar en2fr["friend"] = "ami"
|
469 | setvar en2fr["cat"] = "chat"
|
470 | pp test_ (en2fr => values())
|
471 | ## status: 0
|
472 | ## STDOUT:
|
473 | (List) ["bonjour","ami","chat"]
|
474 | ## END
|
475 |
|
476 | #### Dict -> erase()
|
477 | var book = {title: "The Histories", author: "Herodotus"}
|
478 | call book->erase("author")
|
479 | pp test_ (book)
|
480 | # confirm method is idempotent
|
481 | call book->erase("author")
|
482 | pp test_ (book)
|
483 | ## status: 0
|
484 | ## STDOUT:
|
485 | (Dict) {"title":"The Histories"}
|
486 | (Dict) {"title":"The Histories"}
|
487 | ## END
|
488 |
|
489 | #### Dict -> get()
|
490 | var book = {title: "Hitchhiker's Guide", published: 1979}
|
491 | pp test_ (book => get("title", ""))
|
492 | pp test_ (book => get("published", 0))
|
493 | pp test_ (book => get("author", ""))
|
494 | ## status: 0
|
495 | ## STDOUT:
|
496 | (Str) "Hitchhiker's Guide"
|
497 | (Int) 1979
|
498 | (Str) ""
|
499 | ## END
|
500 |
|
501 | #### Separation of -> attr and () calling
|
502 | const check = "abc" => startsWith
|
503 | pp test_ (check("a"))
|
504 | ## status: 0
|
505 | ## STDOUT:
|
506 | (Bool) true
|
507 | ## END
|
508 |
|
509 | #### Bound methods, receiver value/reference semantics
|
510 | var is_a_ref = { "foo": "bar" }
|
511 | const f = is_a_ref => keys
|
512 | pp test_ (f())
|
513 | setvar is_a_ref["baz"] = 42
|
514 | pp test_ (f())
|
515 |
|
516 | var is_a_val = "abc"
|
517 | const g = is_a_val => startsWith
|
518 | pp test_ (g("a"))
|
519 | setvar is_a_val = "xyz"
|
520 | pp test_ (g("a"))
|
521 | ## status: 0
|
522 | ## STDOUT:
|
523 | (List) ["foo"]
|
524 | (List) ["foo","baz"]
|
525 | (Bool) true
|
526 | (Bool) true
|
527 | ## END
|
528 |
|
529 | #### List => indexOf()
|
530 | var items = [1, '2', 3, { 'a': 5 }]
|
531 |
|
532 | json write (items => indexOf('a'))
|
533 | json write (items => indexOf(1))
|
534 | json write (items => indexOf('2'))
|
535 | json write (items => indexOf({'a': 5}))
|
536 | ## STDOUT:
|
537 | -1
|
538 | 0
|
539 | 1
|
540 | 3
|
541 | ## END
|
542 |
|
543 | #### List => join()
|
544 | var items = [1, 2, 3]
|
545 |
|
546 | json write (items => join()) # default separator is ''
|
547 | json write (items => join(" ")) # explicit separator (can be any number or chars)
|
548 | json write (items => join(", ")) # separator can be any number of chars
|
549 |
|
550 | try {
|
551 | json write (items => join(1)) # separator must be a string
|
552 | }
|
553 | echo "failed with status $_status"
|
554 | ## STDOUT:
|
555 | "123"
|
556 | "1 2 3"
|
557 | "1, 2, 3"
|
558 | failed with status 3
|
559 | ## END
|
560 |
|
561 | #### List->reverse()
|
562 |
|
563 | var empty = []
|
564 |
|
565 | var a = [0]
|
566 | var b = [2, 1, 3]
|
567 | var c = :| hello world |
|
568 |
|
569 | call empty->reverse()
|
570 | call a->reverse()
|
571 | call b->reverse()
|
572 | call c->reverse()
|
573 |
|
574 | pp test_ (empty)
|
575 | pp test_ (a)
|
576 | pp test_ (b)
|
577 | pp test_ (c)
|
578 |
|
579 | ## STDOUT:
|
580 | (List) []
|
581 | (List) [0]
|
582 | (List) [3,1,2]
|
583 | (List) ["world","hello"]
|
584 | ## END
|
585 |
|
586 | #### List->reverse() from iterator
|
587 | var x = list(0 .. 3)
|
588 | call x->reverse()
|
589 | write @x
|
590 | ## STDOUT:
|
591 | 2
|
592 | 1
|
593 | 0
|
594 | ## END
|
595 |
|