mirror of
https://github.com/ruby/ruby.git
synced 2022-11-09 12:17:21 -05:00
* [ruby/irb] Update help message for next context-mode of 4 While here, fixing tab/space issues in help message, and sync rdoc for IRB class to match the help message.ef8e3901cc* [ruby/irb] Do not continue line if last expression is an endless range Fixes [Bug #14824]63414f8465* [ruby/irb] Add a test for not continuing when endless range at eol1020ac9c65* [ruby/irb] Make save-history extension safe for concurrent use This makes the save-history extension check for modifications to the history file before saving it. If the history file was modified after the history was loaded and before it was saved, append only the new history lines to the history file. This can result in more lines in the history file than SAVE_HISTORY allows. However, that will be fixed the next time irb is run and the history is saved. Fixes [Bug #13654]041ef53845* Fix errors when XDG_CONFIG_HOME points to non-writable directory `$HOME/.config` is not writable on CI because I think tests should not corrupt user's data. And GitHub Actions CI sets `XDG_CONFIG_HOME` since `Version: 20210309.1`. https://github.com/ruby/actions/runs/2130811016?check_suite_focus=true#step:16:301 ``` Errno::EACCES: Permission denied @ dir_s_mkdir - /home/runner/.config/irb ``` * Try to fix errors in TestIRB::TestHistory too https://github.com/ruby/actions/runs/2137935523?check_suite_focus=true#step:9:562 ``` 1) Error: TestIRB::TestHistory#test_history_concurrent_use: Errno::EACCES: Permission denied @ dir_s_mkdir - /home/runner/.config/irb /home/runner/work/actions/actions/ruby/lib/fileutils.rb:253:in `mkdir' /home/runner/work/actions/actions/ruby/lib/fileutils.rb:253:in `fu_mkdir' /home/runner/work/actions/actions/ruby/lib/fileutils.rb:231:in `block (2 levels) in mkdir_p' /home/runner/work/actions/actions/ruby/lib/fileutils.rb:229:in `reverse_each' /home/runner/work/actions/actions/ruby/lib/fileutils.rb:229:in `block in mkdir_p' /home/runner/work/actions/actions/ruby/lib/fileutils.rb:211:in `each' /home/runner/work/actions/actions/ruby/lib/fileutils.rb:211:in `mkdir_p' /home/runner/work/actions/actions/ruby/lib/irb/init.rb:355:in `rc_file_generators' /home/runner/work/actions/actions/ruby/lib/irb/init.rb:330:in `rc_file' /home/runner/work/actions/actions/ruby/test/irb/test_history.rb:170:in `block in assert_history' /home/runner/work/actions/actions/ruby/lib/tmpdir.rb:96:in `mktmpdir' /home/runner/work/actions/actions/ruby/test/irb/test_history.rb:168:in `assert_history' /home/runner/work/actions/actions/ruby/test/irb/test_history.rb:133:in `test_history_concurrent_use' ``` * [ruby/irb] Define "measure" command without forced override9587ba13b5* [ruby/irb] Add all lib files automaticallyecc82336b7* [ruby/irb] Don't call Ruby 2.4+'s String#pretty_print89bcf107be* [ruby/irb] Implement ls command19b6c20604* [ruby/irb] Add whereami commandbc822e4aac* [ruby/irb] Fix column overflow on ls output6115754623* [ruby/irb] Fix step's argument cols.size was calling Integer#size, which returns 8. Fixing a bug of https://github.com/ruby/irb/pull/209c93ae4be71* [ruby/irb] Deal with different screen sizes e.g. http://rubyci.s3.amazonaws.com/centos8/ruby-master/log/20210321T063003Z.fail.html.gzddb3472ba2* [ruby/irb] Have some right padding instead of filling out an entire line6ac8f45f5f* Suppress verbose messages Get rid of warnings in the parallel test. ``` unknown command: "Switch to inspect mode." ``` * [ruby/irb] Change ripper_lex_without_warning to a class methodd9f8abc17e* [ruby/irb] Complete require and require_relative1c61178b4c* [ruby/reline] Add Reline.ungetc to control buffer43ac03c624* [ruby/reline] Reline.delete_text removes the current line in multilineda90c094a1* [ruby/reline] Support preposing and postposing for Reline.completion_proc1f469de90c* [ruby/reline] Suppress crashing when completer_{quote,word_break}_characters is emptyc6f1164942* [ruby/irb] fix completion test when out-of-place build * [ruby/irb] Cache completion files to require612ebcb311* [ruby/irb] Always add input method when calling Irb.new in tests When passes input method as nil to Context.new through Irb.new, ReidlineInputMethod.new is executed and the global internal state of Reline is rewritten, therefore other tests are failed in the Ruby repository. This commit changes to use TestInputMethod.010dce9210* [ruby/irb] Prevent the completion from crashing if rdoc is missing There are cases where ruby is installed without rdoc and e.g. lib/irb/cmd/help.rb also handles the LoadError Here is how to replicate the issue: ``` $ docker run -it alpine:3.13.3 sh / # apk add ruby ruby-irb ruby-io-console / # irb irb(main):001:0> Class[TAB][TAB] ``` And you end up with something like: ``` irb(main):001:0> ClassTraceback (most recent call last): 34: from /usr/bin/irb:23:in `<main>' 33: from /usr/bin/irb:23:in `load' 32: from /usr/lib/ruby/gems/2.7.0/gems/irb-1.2.6/exe/irb:11:in `<top (required)>' 31: from /usr/lib/ruby/2.7.0/irb.rb:400:in `start' 30: from /usr/lib/ruby/2.7.0/irb.rb:471:in `run' 29: from /usr/lib/ruby/2.7.0/irb.rb:471:in `catch' 28: from /usr/lib/ruby/2.7.0/irb.rb:472:in `block in run' 27: from /usr/lib/ruby/2.7.0/irb.rb:537:in `eval_input' 26: from /usr/lib/ruby/2.7.0/irb/ruby-lex.rb:150:in `each_top_level_statement' 25: from /usr/lib/ruby/2.7.0/irb/ruby-lex.rb:150:in `catch' 24: from /usr/lib/ruby/2.7.0/irb/ruby-lex.rb:151:in `block in each_top_level_statement' 23: from /usr/lib/ruby/2.7.0/irb/ruby-lex.rb:151:in `loop' 22: from /usr/lib/ruby/2.7.0/irb/ruby-lex.rb:154:in `block (2 levels) in each_top_level_statement' 21: from /usr/lib/ruby/2.7.0/irb/ruby-lex.rb:182:in `lex' 20: from /usr/lib/ruby/2.7.0/irb.rb:518:in `block in eval_input' 19: from /usr/lib/ruby/2.7.0/irb.rb:704:in `signal_status' 18: from /usr/lib/ruby/2.7.0/irb.rb:519:in `block (2 levels) in eval_input' 17: from /usr/lib/ruby/2.7.0/irb/input-method.rb:294:in `gets' 16: from /usr/lib/ruby/2.7.0/forwardable.rb:235:in `readmultiline' 15: from /usr/lib/ruby/2.7.0/forwardable.rb:235:in `readmultiline' 14: from /usr/lib/ruby/2.7.0/reline.rb:175:in `readmultiline' 13: from /usr/lib/ruby/2.7.0/reline.rb:238:in `inner_readline' 12: from /usr/lib/ruby/2.7.0/reline.rb:238:in `loop' 11: from /usr/lib/ruby/2.7.0/reline.rb:239:in `block in inner_readline' 10: from /usr/lib/ruby/2.7.0/reline.rb:270:in `read_io' 9: from /usr/lib/ruby/2.7.0/reline.rb:270:in `loop' 8: from /usr/lib/ruby/2.7.0/reline.rb:311:in `block in read_io' 7: from /usr/lib/ruby/2.7.0/reline.rb:240:in `block (2 levels) in inner_readline' 6: from /usr/lib/ruby/2.7.0/reline.rb:240:in `each' 5: from /usr/lib/ruby/2.7.0/reline.rb:241:in `block (3 levels) in inner_readline' 4: from /usr/lib/ruby/2.7.0/reline/line_editor.rb:820:in `input_key' 3: from /usr/lib/ruby/2.7.0/reline/line_editor.rb:608:in `complete' 2: from /usr/lib/ruby/2.7.0/irb/completion.rb:269:in `block in <module:InputCompletor>' 1: from /usr/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:92:in `require' /usr/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:92:in `require': cannot load such file -- rdoc (LoadError) ```a2d299c2ac* [ruby/irb] Suppress verbose messages in the parallel test `:VERBOSE` flag needs to be set prior to `IRB::Irb.new`.78604682d9* [ruby/irb] SIGINT should raise Interrupt after IRB session5832cfe75b* [ruby/irb] Colorize `__END__` as keyword9b84018311* [ruby/irb] Add show_source command108cb04352* [ruby/reline] Reset @rest_height when clear screen3a7019b0d5* [ruby/irb] process multi-line pastes as a single entity this allows pasting leading-dot chained methods correctly: ```ruby class A def a; self; end def b; true; end end a = A.new a .a .b ``` will properly return `true` instead of erroring on the `.a` line: ``` irb(main):001:1* class A irb(main):002:1* def a; self; end irb(main):003:0> end irb(main):004:0* irb(main):005:0> a = A.new irb(main):006:0* irb(main):007:0> a irb(main):008:0> .a irb(main):009:0> .a => #<A:0x00007f984211fbe8> ```45aeb52575* [ruby/irb] Add yamatanooroti test example279155fcee* [ruby/irb] Add test for multiline pastee93c9cb54d* [ruby/irb] Evaluate each toplevel statementbc1b1d8bc3* [ruby/irb] Version 1.3.522e2ddf715* [ruby/reline] Version 0.2.522ce5651e5Co-authored-by: Jeremy Evans <code@jeremyevans.net> Co-authored-by: Kazuhiro NISHIYAMA <zn@mbf.nifty.com> Co-authored-by: Takashi Kokubun <takashikkbn@gmail.com> Co-authored-by: Nobuyoshi Nakada <nobu@ruby-lang.org> Co-authored-by: Aleksandar Ivanov <aivanov92@gmail.com> Co-authored-by: Koichi Sasada <ko1@atdot.net> Co-authored-by: Cody Cutrer <cody@instructure.com>
403 lines
11 KiB
Ruby
403 lines
11 KiB
Ruby
# frozen_string_literal: false
|
|
#
|
|
# irb/completion.rb -
|
|
# $Release Version: 0.9$
|
|
# $Revision$
|
|
# by Keiju ISHITSUKA(keiju@ishitsuka.com)
|
|
# From Original Idea of shugo@ruby-lang.org
|
|
#
|
|
|
|
require_relative 'ruby-lex'
|
|
|
|
module IRB
|
|
module InputCompletor # :nodoc:
|
|
|
|
|
|
# Set of reserved words used by Ruby, you should not use these for
|
|
# constants or variables
|
|
ReservedWords = %w[
|
|
__ENCODING__ __LINE__ __FILE__
|
|
BEGIN END
|
|
alias and
|
|
begin break
|
|
case class
|
|
def defined? do
|
|
else elsif end ensure
|
|
false for
|
|
if in
|
|
module
|
|
next nil not
|
|
or
|
|
redo rescue retry return
|
|
self super
|
|
then true
|
|
undef unless until
|
|
when while
|
|
yield
|
|
]
|
|
|
|
BASIC_WORD_BREAK_CHARACTERS = " \t\n`><=;|&{("
|
|
|
|
def self.retrieve_files_to_require_from_load_path
|
|
@@files_from_load_path ||= $LOAD_PATH.flat_map { |path|
|
|
begin
|
|
Dir.glob("**/*.{rb,#{RbConfig::CONFIG['DLEXT']}}", base: path)
|
|
rescue Errno::ENOENT
|
|
[]
|
|
end
|
|
}.uniq.map { |path|
|
|
path.sub(/\.(rb|#{RbConfig::CONFIG['DLEXT']})\z/, '')
|
|
}
|
|
end
|
|
|
|
def self.retrieve_files_to_require_relative_from_current_dir
|
|
@@files_from_current_dir ||= Dir.glob("**/*.{rb,#{RbConfig::CONFIG['DLEXT']}}", base: '.').map { |path|
|
|
path.sub(/\.(rb|#{RbConfig::CONFIG['DLEXT']})\z/, '')
|
|
}
|
|
end
|
|
|
|
CompletionRequireProc = lambda { |target, preposing = nil, postposing = nil|
|
|
if target =~ /\A(['"])([^'"]+)\Z/
|
|
quote = $1
|
|
actual_target = $2
|
|
else
|
|
return nil # It's not String literal
|
|
end
|
|
tokens = RubyLex.ripper_lex_without_warning(preposing.gsub(/\s*\z/, ''))
|
|
tok = nil
|
|
tokens.reverse_each do |t|
|
|
unless [:on_lparen, :on_sp, :on_ignored_sp, :on_nl, :on_ignored_nl, :on_comment].include?(t.event)
|
|
tok = t
|
|
break
|
|
end
|
|
end
|
|
result = []
|
|
if tok && tok.event == :on_ident && tok.state == Ripper::EXPR_CMDARG
|
|
case tok.tok
|
|
when 'require'
|
|
result = retrieve_files_to_require_from_load_path.select { |path|
|
|
path.start_with?(actual_target)
|
|
}.map { |path|
|
|
quote + path
|
|
}
|
|
when 'require_relative'
|
|
result = retrieve_files_to_require_relative_from_current_dir.select { |path|
|
|
path.start_with?(actual_target)
|
|
}.map { |path|
|
|
quote + path
|
|
}
|
|
end
|
|
end
|
|
result
|
|
}
|
|
|
|
CompletionProc = lambda { |target, preposing = nil, postposing = nil|
|
|
if preposing && postposing
|
|
result = CompletionRequireProc.(target, preposing, postposing)
|
|
unless result
|
|
result = retrieve_completion_data(target).compact.map{ |i| i.encode(Encoding.default_external) }
|
|
end
|
|
result
|
|
else
|
|
retrieve_completion_data(target).compact.map{ |i| i.encode(Encoding.default_external) }
|
|
end
|
|
}
|
|
|
|
def self.retrieve_completion_data(input, bind: IRB.conf[:MAIN_CONTEXT].workspace.binding, doc_namespace: false)
|
|
case input
|
|
when /^((["'`]).*\2)\.([^.]*)$/
|
|
# String
|
|
receiver = $1
|
|
message = $3
|
|
|
|
candidates = String.instance_methods.collect{|m| m.to_s}
|
|
if doc_namespace
|
|
"String.#{message}"
|
|
else
|
|
select_message(receiver, message, candidates)
|
|
end
|
|
|
|
when /^(\/[^\/]*\/)\.([^.]*)$/
|
|
# Regexp
|
|
receiver = $1
|
|
message = $2
|
|
|
|
candidates = Regexp.instance_methods.collect{|m| m.to_s}
|
|
if doc_namespace
|
|
"Regexp.#{message}"
|
|
else
|
|
select_message(receiver, message, candidates)
|
|
end
|
|
|
|
when /^([^\]]*\])\.([^.]*)$/
|
|
# Array
|
|
receiver = $1
|
|
message = $2
|
|
|
|
candidates = Array.instance_methods.collect{|m| m.to_s}
|
|
if doc_namespace
|
|
"Array.#{message}"
|
|
else
|
|
select_message(receiver, message, candidates)
|
|
end
|
|
|
|
when /^([^\}]*\})\.([^.]*)$/
|
|
# Proc or Hash
|
|
receiver = $1
|
|
message = $2
|
|
|
|
proc_candidates = Proc.instance_methods.collect{|m| m.to_s}
|
|
hash_candidates = Hash.instance_methods.collect{|m| m.to_s}
|
|
if doc_namespace
|
|
["Proc.#{message}", "Hash.#{message}"]
|
|
else
|
|
select_message(receiver, message, proc_candidates | hash_candidates)
|
|
end
|
|
|
|
when /^(:[^:.]*)$/
|
|
# Symbol
|
|
return nil if doc_namespace
|
|
sym = $1
|
|
candidates = Symbol.all_symbols.collect do |s|
|
|
":" + s.id2name.encode(Encoding.default_external)
|
|
rescue Encoding::UndefinedConversionError
|
|
# ignore
|
|
end
|
|
candidates.grep(/^#{Regexp.quote(sym)}/)
|
|
|
|
when /^::([A-Z][^:\.\(]*)$/
|
|
# Absolute Constant or class methods
|
|
receiver = $1
|
|
candidates = Object.constants.collect{|m| m.to_s}
|
|
if doc_namespace
|
|
candidates.find { |i| i == receiver }
|
|
else
|
|
candidates.grep(/^#{receiver}/).collect{|e| "::" + e}
|
|
end
|
|
|
|
when /^([A-Z].*)::([^:.]*)$/
|
|
# Constant or class methods
|
|
receiver = $1
|
|
message = $2
|
|
begin
|
|
candidates = eval("#{receiver}.constants.collect{|m| m.to_s}", bind)
|
|
candidates |= eval("#{receiver}.methods.collect{|m| m.to_s}", bind)
|
|
rescue Exception
|
|
candidates = []
|
|
end
|
|
if doc_namespace
|
|
"#{receiver}::#{message}"
|
|
else
|
|
select_message(receiver, message, candidates, "::")
|
|
end
|
|
|
|
when /^(:[^:.]+)(\.|::)([^.]*)$/
|
|
# Symbol
|
|
receiver = $1
|
|
sep = $2
|
|
message = $3
|
|
|
|
candidates = Symbol.instance_methods.collect{|m| m.to_s}
|
|
if doc_namespace
|
|
"Symbol.#{message}"
|
|
else
|
|
select_message(receiver, message, candidates, sep)
|
|
end
|
|
|
|
when /^(?<num>-?(?:0[dbo])?[0-9_]+(?:\.[0-9_]+)?(?:(?:[eE][+-]?[0-9]+)?i?|r)?)(?<sep>\.|::)(?<mes>[^.]*)$/
|
|
# Numeric
|
|
receiver = $~[:num]
|
|
sep = $~[:sep]
|
|
message = $~[:mes]
|
|
|
|
begin
|
|
instance = eval(receiver, bind)
|
|
if doc_namespace
|
|
"#{instance.class.name}.#{message}"
|
|
else
|
|
candidates = instance.methods.collect{|m| m.to_s}
|
|
select_message(receiver, message, candidates, sep)
|
|
end
|
|
rescue Exception
|
|
if doc_namespace
|
|
nil
|
|
else
|
|
candidates = []
|
|
end
|
|
end
|
|
|
|
when /^(-?0x[0-9a-fA-F_]+)(\.|::)([^.]*)$/
|
|
# Numeric(0xFFFF)
|
|
receiver = $1
|
|
sep = $2
|
|
message = $3
|
|
|
|
begin
|
|
instance = eval(receiver, bind)
|
|
if doc_namespace
|
|
"#{instance.class.name}.#{message}"
|
|
else
|
|
candidates = instance.methods.collect{|m| m.to_s}
|
|
select_message(receiver, message, candidates, sep)
|
|
end
|
|
rescue Exception
|
|
if doc_namespace
|
|
nil
|
|
else
|
|
candidates = []
|
|
end
|
|
end
|
|
|
|
when /^(\$[^.]*)$/
|
|
# global var
|
|
gvar = $1
|
|
all_gvars = global_variables.collect{|m| m.to_s}
|
|
if doc_namespace
|
|
all_gvars.find{ |i| i == gvar }
|
|
else
|
|
all_gvars.grep(Regexp.new(Regexp.quote(gvar)))
|
|
end
|
|
|
|
when /^([^."].*)(\.|::)([^.]*)$/
|
|
# variable.func or func.func
|
|
receiver = $1
|
|
sep = $2
|
|
message = $3
|
|
|
|
gv = eval("global_variables", bind).collect{|m| m.to_s}.push("true", "false", "nil")
|
|
lv = eval("local_variables", bind).collect{|m| m.to_s}
|
|
iv = eval("instance_variables", bind).collect{|m| m.to_s}
|
|
cv = eval("self.class.constants", bind).collect{|m| m.to_s}
|
|
|
|
if (gv | lv | iv | cv).include?(receiver) or /^[A-Z]/ =~ receiver && /\./ !~ receiver
|
|
# foo.func and foo is var. OR
|
|
# foo::func and foo is var. OR
|
|
# foo::Const and foo is var. OR
|
|
# Foo::Bar.func
|
|
begin
|
|
candidates = []
|
|
rec = eval(receiver, bind)
|
|
if sep == "::" and rec.kind_of?(Module)
|
|
candidates = rec.constants.collect{|m| m.to_s}
|
|
end
|
|
candidates |= rec.methods.collect{|m| m.to_s}
|
|
rescue Exception
|
|
candidates = []
|
|
end
|
|
else
|
|
# func1.func2
|
|
candidates = []
|
|
to_ignore = ignored_modules
|
|
ObjectSpace.each_object(Module){|m|
|
|
next if (to_ignore.include?(m) rescue true)
|
|
candidates.concat m.instance_methods(false).collect{|x| x.to_s}
|
|
}
|
|
candidates.sort!
|
|
candidates.uniq!
|
|
end
|
|
if doc_namespace
|
|
"#{rec.class.name}#{sep}#{candidates.find{ |i| i == message }}"
|
|
else
|
|
select_message(receiver, message, candidates, sep)
|
|
end
|
|
|
|
when /^\.([^.]*)$/
|
|
# unknown(maybe String)
|
|
|
|
receiver = ""
|
|
message = $1
|
|
|
|
candidates = String.instance_methods(true).collect{|m| m.to_s}
|
|
if doc_namespace
|
|
"String.#{candidates.find{ |i| i == message }}"
|
|
else
|
|
select_message(receiver, message, candidates)
|
|
end
|
|
|
|
else
|
|
candidates = eval("methods | private_methods | local_variables | instance_variables | self.class.constants", bind).collect{|m| m.to_s}
|
|
candidates |= ReservedWords
|
|
|
|
if doc_namespace
|
|
candidates.find{ |i| i == input }
|
|
else
|
|
candidates.grep(/^#{Regexp.quote(input)}/)
|
|
end
|
|
end
|
|
end
|
|
|
|
PerfectMatchedProc = ->(matched, bind: IRB.conf[:MAIN_CONTEXT].workspace.binding) {
|
|
begin
|
|
require 'rdoc'
|
|
rescue LoadError
|
|
return
|
|
end
|
|
|
|
RDocRIDriver ||= RDoc::RI::Driver.new
|
|
|
|
if matched =~ /\A(?:::)?RubyVM/ and not ENV['RUBY_YES_I_AM_NOT_A_NORMAL_USER']
|
|
IRB.__send__(:easter_egg)
|
|
return
|
|
end
|
|
|
|
namespace = retrieve_completion_data(matched, bind: bind, doc_namespace: true)
|
|
return unless namespace
|
|
|
|
if namespace.is_a?(Array)
|
|
out = RDoc::Markup::Document.new
|
|
namespace.each do |m|
|
|
begin
|
|
RDocRIDriver.add_method(out, m)
|
|
rescue RDoc::RI::Driver::NotFoundError
|
|
end
|
|
end
|
|
RDocRIDriver.display(out)
|
|
else
|
|
begin
|
|
RDocRIDriver.display_names([namespace])
|
|
rescue RDoc::RI::Driver::NotFoundError
|
|
end
|
|
end
|
|
}
|
|
|
|
# Set of available operators in Ruby
|
|
Operators = %w[% & * ** + - / < << <= <=> == === =~ > >= >> [] []= ^ ! != !~]
|
|
|
|
def self.select_message(receiver, message, candidates, sep = ".")
|
|
candidates.grep(/^#{Regexp.quote(message)}/).collect do |e|
|
|
case e
|
|
when /^[a-zA-Z_]/
|
|
receiver + sep + e
|
|
when /^[0-9]/
|
|
when *Operators
|
|
#receiver + " " + e
|
|
end
|
|
end
|
|
end
|
|
|
|
def self.ignored_modules
|
|
# We could cache the result, but this is very fast already.
|
|
# By using this approach, we avoid Module#name calls, which are
|
|
# relatively slow when there are a lot of anonymous modules defined.
|
|
s = {}
|
|
|
|
scanner = lambda do |m|
|
|
next if s.include?(m) # IRB::ExtendCommandBundle::EXCB recurses.
|
|
s[m] = true
|
|
m.constants(false).each do |c|
|
|
value = m.const_get(c)
|
|
scanner.call(value) if value.is_a?(Module)
|
|
end
|
|
end
|
|
|
|
%i(IRB RubyLex).each do |sym|
|
|
next unless Object.const_defined?(sym)
|
|
scanner.call(Object.const_get(sym))
|
|
end
|
|
|
|
s.delete(IRB::Context) if defined?(IRB::Context)
|
|
|
|
s
|
|
end
|
|
end
|
|
end
|