mirror of https://github.com/patacrep/patacrep.git
Oliverpool
9 years ago
31 changed files with 843 additions and 152 deletions
@ -0,0 +1,103 @@ |
|||
"""Raw songbook utilities""" |
|||
|
|||
import logging |
|||
import os |
|||
import sys |
|||
import yaml |
|||
|
|||
from patacrep import encoding |
|||
from patacrep.build import config_model |
|||
from patacrep.utils import DictOfDict |
|||
from patacrep.songs import DataSubpath |
|||
import patacrep |
|||
|
|||
LOGGER = logging.getLogger() |
|||
|
|||
def open_songbook(filename): |
|||
"""Open songbook, and return a raw songbook object. |
|||
|
|||
:param str filename: Filename of the yaml songbook. |
|||
:rvalue: dict |
|||
:return: Songbook, as a dictionary. |
|||
""" |
|||
if os.path.exists(filename + ".yaml") and not os.path.exists(filename): |
|||
filename += ".yaml" |
|||
|
|||
try: |
|||
with patacrep.encoding.open_read(filename) as songbook_file: |
|||
user_songbook = yaml.load(songbook_file) |
|||
if 'encoding' in user_songbook.get('book', []): |
|||
with encoding.open_read( |
|||
filename, |
|||
encoding=user_songbook['book']['encoding'] |
|||
) as songbook_file: |
|||
user_songbook = yaml.load(songbook_file) |
|||
except Exception as error: # pylint: disable=broad-except |
|||
raise patacrep.errors.SongbookError(str(error)) |
|||
|
|||
songbook = _add_songbook_defaults(user_songbook) |
|||
|
|||
songbook['_filepath'] = filename |
|||
songbook['_basename'] = os.path.basename(filename)[:-len(".yaml")] |
|||
|
|||
# Gathering datadirs |
|||
songbook['_datadir'] = list(_iter_absolute_datadirs(songbook)) |
|||
if 'datadir' in songbook['book']: |
|||
del songbook['book']['datadir'] |
|||
|
|||
songbook['_songdir'] = [ |
|||
DataSubpath(path, 'songs') |
|||
for path in songbook['_datadir'] |
|||
] |
|||
|
|||
|
|||
return songbook |
|||
|
|||
def _add_songbook_defaults(user_songbook): |
|||
""" Adds the defaults values to the songbook if missing from |
|||
the user songbook |
|||
|
|||
Priority: |
|||
- User values |
|||
- Default values of the user lang (if set) |
|||
- Default english values |
|||
""" |
|||
|
|||
# Merge the default and user configs |
|||
locale_default = config_model('default') |
|||
# Initialize with default in english |
|||
default_songbook = locale_default.get('en', {}) |
|||
default_songbook = DictOfDict(default_songbook) |
|||
|
|||
if 'lang' in user_songbook.get('book', []): |
|||
# Update default with current lang |
|||
lang = user_songbook['book']['lang'] |
|||
default_songbook.update(locale_default.get(lang, {})) |
|||
# Update default with user_songbook |
|||
default_songbook.update(user_songbook) |
|||
|
|||
return dict(default_songbook) |
|||
|
|||
def _iter_absolute_datadirs(raw_songbook): |
|||
"""Iterate on the absolute datadirs of the raw songbook |
|||
|
|||
Appends the songfile dir at the end |
|||
""" |
|||
datadir = raw_songbook.get('book', {}).get('datadir') |
|||
filepath = raw_songbook['_filepath'] |
|||
|
|||
if datadir is None: |
|||
datadir = [] |
|||
elif isinstance(datadir, str): |
|||
datadir = [datadir] |
|||
basedir = os.path.dirname(os.path.abspath(filepath)) |
|||
|
|||
for path in datadir: |
|||
abspath = os.path.join(basedir, path) |
|||
if os.path.exists(abspath) and os.path.isdir(abspath): |
|||
yield abspath |
|||
else: |
|||
LOGGER.warning( |
|||
"Ignoring non-existent datadir '{}'.".format(path) |
|||
) |
|||
yield basedir |
@ -0,0 +1,114 @@ |
|||
#!/bin/env python3 |
|||
|
|||
"""Command line client to :mod:`tools`""" |
|||
|
|||
import argparse |
|||
import logging |
|||
import operator |
|||
import os |
|||
import pkgutil |
|||
import re |
|||
import sys |
|||
|
|||
import patacrep |
|||
|
|||
# Logging configuration |
|||
logging.basicConfig(level=logging.INFO) |
|||
LOGGER = logging.getLogger("patatools") |
|||
|
|||
def _execlp(program, args): |
|||
"""Call :func:`os.execlp`, adding `program` as the first argument to itself.""" |
|||
return os.execlp(program, program, *args) |
|||
|
|||
def _iter_subcommands(): |
|||
"""Iterate over subcommands. |
|||
|
|||
The objects returned are tuples of: |
|||
- the name of the command; |
|||
- its description; |
|||
- the function to call to execute the subcommand. |
|||
""" |
|||
subcommands = [] |
|||
|
|||
# Get python subcommands |
|||
path = [os.path.join(item, "patacrep", "tools") for item in sys.path] |
|||
prefix = "patacrep.tools." |
|||
module_re = re.compile(r'{}(?P<subcommand>[^\.]*)\.__main__'.format(prefix)) |
|||
for module_loader, name, _ in pkgutil.walk_packages(path, prefix): |
|||
match = module_re.match(name) |
|||
if match: |
|||
module = module_loader.find_module(match.string).load_module() |
|||
if hasattr(module, "SUBCOMMAND_DESCRIPTION"): |
|||
subcommands.append(match.groupdict()['subcommand']) |
|||
yield ( |
|||
match.groupdict()['subcommand'], |
|||
getattr(module, "SUBCOMMAND_DESCRIPTION"), |
|||
module.main, |
|||
) |
|||
|
|||
class ArgumentParser(argparse.ArgumentParser): |
|||
"""Proxy class to circumvent an :mod:`argparse` bug. |
|||
|
|||
Contrarily to what documented, the `argparse.REMAINDER |
|||
<https://docs.python.org/3/library/argparse.html#nargs>`_ `nargs` setting |
|||
does not include the remainder arguments if the first one begins with `-`. |
|||
|
|||
This bug is reperted as `17050 <https://bugs.python.org/issue17050>`_. This |
|||
class can be deleted once this bug has been fixed. |
|||
""" |
|||
|
|||
def parse_args(self, args=None, namespace=None): |
|||
if args is None: |
|||
args = sys.argv[1:] |
|||
subcommands = [command[0] for command in set(_iter_subcommands())] |
|||
if len(args) > 0: |
|||
if args[0] in subcommands: |
|||
args = [args[0], "--"] + args[1:] |
|||
|
|||
value = super().parse_args(args, namespace) |
|||
|
|||
if hasattr(value, 'remainder'): |
|||
value.remainder = value.remainder[1:] |
|||
return value |
|||
|
|||
|
|||
def commandline_parser(): |
|||
"""Return a command line parser.""" |
|||
|
|||
parser = ArgumentParser( |
|||
prog="patatools", |
|||
description=( |
|||
"Miscellaneous tools for patacrep." |
|||
), |
|||
formatter_class=argparse.RawTextHelpFormatter, |
|||
) |
|||
|
|||
parser.add_argument( |
|||
'--version', |
|||
help='Show version', |
|||
action='version', |
|||
version='%(prog)s ' + patacrep.__version__ |
|||
) |
|||
|
|||
subparsers = parser.add_subparsers( |
|||
title="Subcommands", |
|||
description="List of available subcommands.", |
|||
) |
|||
subparsers.required = True |
|||
subparsers.dest = "subcommand" |
|||
|
|||
for command, message, function in sorted(_iter_subcommands(), key=operator.itemgetter(0)): |
|||
sub1 = subparsers.add_parser(command, help=message, add_help=False) |
|||
sub1.add_argument('remainder', nargs=argparse.REMAINDER) |
|||
sub1.set_defaults(function=function) |
|||
|
|||
return parser |
|||
|
|||
def main(args): |
|||
"""Main function""" |
|||
parser = commandline_parser() |
|||
args = parser.parse_args(args[1:]) |
|||
args.function(["patatools-{}".format(args.subcommand)] + args.remainder) |
|||
|
|||
if __name__ == "__main__": |
|||
main(sys.argv) |
@ -0,0 +1,73 @@ |
|||
"""`patatools cache` command: cache manipulation.""" |
|||
|
|||
import argparse |
|||
import logging |
|||
import os |
|||
import shutil |
|||
import sys |
|||
import textwrap |
|||
|
|||
from patacrep import errors |
|||
from patacrep.songbook import open_songbook |
|||
|
|||
LOGGER = logging.getLogger("patatools.cache") |
|||
SUBCOMMAND_DESCRIPTION = "Perform operations on cache." |
|||
|
|||
def filename(name): |
|||
"""Check that argument is an existing, readable file name. |
|||
|
|||
Return the argument for convenience. |
|||
""" |
|||
if os.path.isfile(name) and os.access(name, os.R_OK): |
|||
return name |
|||
raise argparse.ArgumentTypeError("Cannot read file '{}'.".format(name)) |
|||
|
|||
def commandline_parser(): |
|||
"""Return a command line parser.""" |
|||
|
|||
parser = argparse.ArgumentParser( |
|||
prog="patatools cache", |
|||
description=SUBCOMMAND_DESCRIPTION, |
|||
formatter_class=argparse.RawTextHelpFormatter, |
|||
) |
|||
|
|||
subparsers = parser.add_subparsers( |
|||
description="", |
|||
dest="command", |
|||
) |
|||
subparsers.required = True |
|||
|
|||
clean = subparsers.add_parser( |
|||
"clean", |
|||
description="Delete cache.", |
|||
help="Delete cache.", |
|||
) |
|||
clean.add_argument( |
|||
'songbook', |
|||
metavar="SONGBOOK", |
|||
help=textwrap.dedent("""Songbook file to be used to look for cache path."""), |
|||
type=filename, |
|||
) |
|||
clean.set_defaults(command=do_clean) |
|||
|
|||
return parser |
|||
|
|||
def do_clean(namespace): |
|||
"""Execute the `patatools cache clean` command.""" |
|||
for datadir in open_songbook(namespace.songbook)['_datadir']: |
|||
cachedir = os.path.join(datadir, ".cache") |
|||
LOGGER.info("Deleting cache directory '{}'...".format(cachedir)) |
|||
if os.path.isdir(cachedir): |
|||
shutil.rmtree(cachedir) |
|||
|
|||
def main(args): |
|||
"""Main function: run from command line.""" |
|||
options = commandline_parser().parse_args(args[1:]) |
|||
try: |
|||
options.command(options) |
|||
except errors.SongbookError as error: |
|||
LOGGER.error(str(error)) |
|||
sys.exit(1) |
|||
|
|||
if __name__ == "__main__": |
|||
main(sys.argv) |
@ -0,0 +1,6 @@ |
|||
#! /bin/sh |
|||
|
|||
# Do not edit this file. This file is just a helper file for development test. |
|||
# It is not part of the distributed software. |
|||
|
|||
python -m patacrep.tools $@ |
@ -0,0 +1 @@ |
|||
.cache |
@ -0,0 +1,67 @@ |
|||
"""Tests of the patatools-cache command.""" |
|||
|
|||
# pylint: disable=too-few-public-methods |
|||
|
|||
import os |
|||
import shutil |
|||
import unittest |
|||
|
|||
from patacrep.files import chdir |
|||
from patacrep.tools.__main__ import main as tools_main |
|||
from patacrep.tools.cache.__main__ import main as cache_main |
|||
from patacrep.songbook.__main__ import main as songbook_main |
|||
|
|||
CACHEDIR = os.path.join(os.path.dirname(__file__), "test_cache_datadir", ".cache") |
|||
|
|||
class TestCache(unittest.TestCase): |
|||
"""Test of the "patatools cache" subcommand""" |
|||
|
|||
def setUp(self): |
|||
"""Remove cache.""" |
|||
self._remove_cache() |
|||
self.assertFalse(os.path.exists(CACHEDIR)) |
|||
|
|||
def tearDown(self): |
|||
"""Remove cache.""" |
|||
self._remove_cache() |
|||
self.assertFalse(os.path.exists(CACHEDIR)) |
|||
|
|||
@staticmethod |
|||
def _remove_cache(): |
|||
"""Delete cache.""" |
|||
shutil.rmtree(CACHEDIR, ignore_errors=True) |
|||
|
|||
def _system(self, main, args): |
|||
with chdir(os.path.dirname(__file__)): |
|||
try: |
|||
main(args) |
|||
except SystemExit as systemexit: |
|||
self.assertEqual(systemexit.code, 0) |
|||
|
|||
def test_clean_exists(self): |
|||
"""Test of the "patatools cache clean" subcommand""" |
|||
for main, args in [ |
|||
(tools_main, ["patatools", "cache", "clean", "test_cache.yaml"]), |
|||
(cache_main, ["patatools-cache", "clean", "test_cache.yaml"]), |
|||
]: |
|||
with self.subTest(main=main, args=args): |
|||
# First compilation. Ensure that cache exists afterwards |
|||
self._system(songbook_main, ["songbook", "--steps", "tex,clean", "test_cache.yaml"]) |
|||
self.assertTrue(os.path.exists(CACHEDIR)) |
|||
|
|||
# Clean cache |
|||
self._system(main, args) |
|||
|
|||
# Ensure that cache does not exist |
|||
self.assertFalse(os.path.exists(CACHEDIR)) |
|||
|
|||
def test_clean_not_exists(self): |
|||
"""Test of the "patatools cache clean" subcommand""" |
|||
# Clean non-existent cache |
|||
for main, args in [ |
|||
(tools_main, ["patatools", "cache", "clean", "test_cache.yaml"]), |
|||
(cache_main, ["patatools-cache", "clean", "test_cache.yaml"]), |
|||
]: |
|||
with self.subTest(main=main, args=args): |
|||
# Clean cache |
|||
self._system(main, args) |
@ -0,0 +1,2 @@ |
|||
book: |
|||
datadir: test_cache_datadir |
@ -0,0 +1,123 @@ |
|||
"""Tests of the patatools-convert command.""" |
|||
|
|||
# pylint: disable=too-few-public-methods |
|||
|
|||
import contextlib |
|||
import glob |
|||
import os |
|||
import unittest |
|||
|
|||
from pkg_resources import resource_filename |
|||
|
|||
from patacrep import files |
|||
from patacrep.tools.__main__ import main as tools_main |
|||
from patacrep.encoding import open_read |
|||
from patacrep.tools.convert.__main__ import main as convert_main |
|||
|
|||
from .. import dynamic # pylint: disable=unused-import |
|||
from .. import logging_reduced |
|||
|
|||
class TestConvert(unittest.TestCase, metaclass=dynamic.DynamicTest): |
|||
"""Test of the "patatools convert" subcommand""" |
|||
|
|||
@staticmethod |
|||
def _system(main, args): |
|||
try: |
|||
main(args) |
|||
except SystemExit as systemexit: |
|||
return systemexit.code |
|||
return 0 |
|||
|
|||
def assertConvert(self, basename, in_format, out_format): # pylint: disable=invalid-name |
|||
"""Test of the "patatools convert" subcommand""" |
|||
sourcename = "{}.{}".format(basename, in_format) |
|||
destname = "{}.{}".format(basename, out_format) |
|||
controlname = "{}.{}.control".format(sourcename, out_format) |
|||
for main, args in [ |
|||
(tools_main, ["patatools", "convert"]), |
|||
(convert_main, ["patatools-convert"]), |
|||
]: |
|||
with self.subTest(main=main, args=args): |
|||
with self.chdir("test_convert_success"): |
|||
with open_read(controlname) as controlfile: |
|||
with logging_reduced(): |
|||
if os.path.exists(destname): |
|||
os.remove(destname) |
|||
self.assertEqual( |
|||
self._system(main, args + [in_format, out_format, sourcename]), |
|||
0, |
|||
) |
|||
expected = controlfile.read().strip().replace( |
|||
"@TEST_FOLDER@", |
|||
files.path2posix(resource_filename(__name__, "")), |
|||
) |
|||
with open_read(destname) as destfile: |
|||
self.assertMultiLineEqual( |
|||
destfile.read().replace('\r\n', '\n').strip(), |
|||
expected.strip(), |
|||
) |
|||
|
|||
def assertFailConvert(self, basename, in_format, out_format): # pylint: disable=invalid-name |
|||
"""Test of the "patatools convert" subcommand""" |
|||
sourcename = "{}.{}".format(basename, in_format) |
|||
destname = "{}.{}".format(basename, out_format) |
|||
for main, args in [ |
|||
(tools_main, ["patatools", "convert"]), |
|||
(convert_main, ["patatools-convert"]), |
|||
]: |
|||
with self.subTest(main=main, args=args): |
|||
with self.chdir("test_convert_failure"): |
|||
with logging_reduced(): |
|||
if os.path.exists(destname): |
|||
os.remove(destname) |
|||
self.assertEqual( |
|||
self._system(main, args + [in_format, out_format, sourcename]), |
|||
1, |
|||
) |
|||
|
|||
@staticmethod |
|||
@contextlib.contextmanager |
|||
def chdir(*pathlist): |
|||
"""Temporary change current directory, relative to this file directory""" |
|||
with files.chdir(resource_filename(__name__, ""), *pathlist): |
|||
yield |
|||
|
|||
@classmethod |
|||
def _iter_testmethods(cls): |
|||
"""Iterate over song files to test.""" |
|||
for directory, create_test in [ |
|||
("test_convert_success", cls._create_test_success), |
|||
("test_convert_failure", cls._create_test_failure), |
|||
]: |
|||
with cls.chdir(directory): |
|||
for control in sorted(glob.glob('*.*.*.control')): |
|||
[*base, in_format, out_format, _] = control.split('.') |
|||
base = '.'.join(base) |
|||
yield ( |
|||
"test_{}_{}_{}".format(base, in_format, out_format), |
|||
create_test(base, in_format, out_format), |
|||
) |
|||
|
|||
@classmethod |
|||
def _create_test_success(cls, base, in_format, out_format): |
|||
"""Return a function testing conversion. |
|||
|
|||
:param str base: Base name of the file to convert. |
|||
:param str in_format: Source format. |
|||
:param str out_format: Destination format. |
|||
""" |
|||
test_parse_render = lambda self: self.assertConvert(base, in_format, out_format) |
|||
test_parse_render.__doc__ = ( |
|||
"Test that '{base}.{in_format}' is correctly converted to '{out_format}'." |
|||
).format(base=os.path.basename(base), in_format=in_format, out_format=out_format) |
|||
return test_parse_render |
|||
|
|||
@classmethod |
|||
def _create_test_failure(cls, base, in_format, out_format): |
|||
"""Return a function testing failing conversions |
|||
""" |
|||
test_parse_render = lambda self: self.assertFailConvert(base, in_format, out_format) |
|||
test_parse_render.__doc__ = ( |
|||
"Test that '{base}.{in_format}' raises an error when trying to convert it to '{out_format}'." # pylint: disable=line-too-long |
|||
).format(base=os.path.basename(base), in_format=in_format, out_format=out_format) |
|||
return test_parse_render |
@ -0,0 +1,122 @@ |
|||
\selectlanguage{english} |
|||
|
|||
\beginsong{}[ |
|||
by={ |
|||
}, |
|||
] |
|||
|
|||
|
|||
\begin{verse} |
|||
\selectlanguage |
|||
\songcolumns |
|||
\beginsong |
|||
\[&y={Traditionnel},cover={traditionnel},al&um={France}] |
|||
\end{verse} |
|||
|
|||
|
|||
\begin{verse} |
|||
\cover |
|||
\gtab |
|||
\gtab |
|||
\gtab |
|||
\end{verse} |
|||
|
|||
|
|||
\begin{verse} |
|||
\begin |
|||
Cheva\\[C]liers de la Table Ronde |
|||
Goûtons \\[G7]voir si le vin est \\[C]bon |
|||
\rep |
|||
\end |
|||
\end{verse} |
|||
|
|||
|
|||
\begin{verse} |
|||
\begin |
|||
Goûtons \\[F]voir, \echo |
|||
Goûtons \\[C]voir, \echo |
|||
Goûtons \\[G7]voir si le vin est bon |
|||
\rep |
|||
\end |
|||
\end{verse} |
|||
|
|||
|
|||
\begin{verse} |
|||
\begin |
|||
S'il est bon, s'il est agréable |
|||
J'en boirai jusqu'à mon plaisir |
|||
\end |
|||
\end{verse} |
|||
|
|||
|
|||
\begin{verse} |
|||
\begin |
|||
J'en boirai cinq à six bouteilles |
|||
Et encore, ce n'est pas beaucoup |
|||
\end |
|||
\end{verse} |
|||
|
|||
|
|||
\begin{verse} |
|||
\begin |
|||
Si je meurs, je veux qu'on m'enterre |
|||
Dans une cave où il y a du bon vin |
|||
\end |
|||
\end{verse} |
|||
|
|||
|
|||
\begin{verse} |
|||
\begin |
|||
Les deux pieds contre la muraille |
|||
Et la tête sous le robinet |
|||
\end |
|||
\end{verse} |
|||
|
|||
|
|||
\begin{verse} |
|||
\begin |
|||
Et les quatre plus grands ivrognes |
|||
Porteront les quatre coins du drap |
|||
\end |
|||
\end{verse} |
|||
|
|||
|
|||
\begin{verse} |
|||
\begin |
|||
Pour donner le discours d'usage |
|||
On prendra le bistrot du coin |
|||
\end |
|||
\end{verse} |
|||
|
|||
|
|||
\begin{verse} |
|||
\begin |
|||
Et si le tonneau se débouche |
|||
J'en boirai jusqu'à mon plaisir |
|||
\end |
|||
\end{verse} |
|||
|
|||
|
|||
\begin{verse} |
|||
\begin |
|||
Et s'il en reste quelques gouttes |
|||
Ce sera pour nous rafraîchir |
|||
\end |
|||
\end{verse} |
|||
|
|||
|
|||
\begin{verse} |
|||
\begin |
|||
Sur ma tombe, je veux qu'on inscrive |
|||
\emph |
|||
\end |
|||
\end{verse} |
|||
|
|||
|
|||
\begin{verse} |
|||
\endsong |
|||
\end{verse} |
|||
|
|||
|
|||
|
|||
\endsong |
@ -0,0 +1 @@ |
|||
greensleeves.tsg |
@ -0,0 +1,45 @@ |
|||
{lang: en} |
|||
{columns: 2} |
|||
{title: Greensleeves} |
|||
{title: Un autre sous-titre} |
|||
{title: Un sous titre} |
|||
{artist: Traditionnel} |
|||
{album: Angleterre} |
|||
|
|||
|
|||
A[Am]las, my love, ye [G]do me wrong |
|||
To [Am]cast me oft dis[E]curteously |
|||
And [Am]I have loved [G]you so long |
|||
De[Am]lighting [E]in your [Am]companie |
|||
|
|||
|
|||
{start_of_chorus} |
|||
[C]Green[B]sleeves was [G]all my joy |
|||
[Am]Greensleeves was [E]my delight |
|||
[C]Greensleeves was my [G]heart of gold |
|||
And [Am]who but [E]Ladie [Am]Greensleeves |
|||
{end_of_chorus} |
|||
|
|||
|
|||
I [Am]have been ready [G]at your hand |
|||
To [Am]grant what ever [E]you would crave |
|||
I [Am]have both waged [G]life and land |
|||
Your [Am]love and [E]good will [Am]for to have |
|||
|
|||
|
|||
I [Am]bought thee kerchers [G]to thy head |
|||
That [Am]were wrought fine and [E]gallantly |
|||
I [Am]kept thee both at [G]boord and bed |
|||
Which [Am]cost my [E]purse well [Am]favouredly |
|||
|
|||
|
|||
I [Am]bought thee peticotes [G]of the best |
|||
The [Am]cloth so fine as [E]fine might be |
|||
I [Am]gave thee jewels [G]for thy chest |
|||
And [Am]all this [E]cost I [Am]spent on thee |
|||
|
|||
|
|||
Thy [Am]smock of silke, both [G]faire and white |
|||
With [Am]gold embrodered [E]gorgeously |
|||
Thy [Am]peticote of [G]sendall right |
|||
And [Am]this I [E]bought thee [Am]gladly |
@ -0,0 +1,62 @@ |
|||
\selectlanguage{english} |
|||
\songcolumns{2} |
|||
|
|||
\beginsong{Greensleeves\\ |
|||
Un autre sous-titre\\ |
|||
Un sous titre}[ |
|||
by={ |
|||
Traditionnel }, |
|||
album={Angleterre}, |
|||
] |
|||
|
|||
|
|||
|
|||
|
|||
\begin{verse} |
|||
A\[Am]las, my love, ye \[G]do me wrong |
|||
To \[Am]cast me oft dis\[E]curteously |
|||
And \[Am]I have loved \[G]you so long |
|||
De\[Am]lighting \[E]in your \[Am]companie |
|||
\end{verse} |
|||
|
|||
|
|||
\begin{chorus} |
|||
\[C]Green\[B]sleeves was \[G]all my joy |
|||
\[Am]Greensleeves was \[E]my delight |
|||
\[C]Greensleeves was my \[G]heart of gold |
|||
And \[Am]who but \[E]Ladie \[Am]Greensleeves |
|||
\end{chorus} |
|||
|
|||
|
|||
\begin{verse} |
|||
I \[Am]have been ready \[G]at your hand |
|||
To \[Am]grant what ever \[E]you would crave |
|||
I \[Am]have both waged \[G]life and land |
|||
Your \[Am]love and \[E]good will \[Am]for to have |
|||
\end{verse} |
|||
|
|||
|
|||
\begin{verse} |
|||
I \[Am]bought thee kerchers \[G]to thy head |
|||
That \[Am]were wrought fine and \[E]gallantly |
|||
I \[Am]kept thee both at \[G]boord and bed |
|||
Which \[Am]cost my \[E]purse well \[Am]favouredly |
|||
\end{verse} |
|||
|
|||
|
|||
\begin{verse} |
|||
I \[Am]bought thee peticotes \[G]of the best |
|||
The \[Am]cloth so fine as \[E]fine might be |
|||
I \[Am]gave thee jewels \[G]for thy chest |
|||
And \[Am]all this \[E]cost I \[Am]spent on thee |
|||
\end{verse} |
|||
|
|||
|
|||
\begin{verse} |
|||
Thy \[Am]smock of silke, both \[G]faire and white |
|||
With \[Am]gold embrodered \[E]gorgeously |
|||
Thy \[Am]peticote of \[G]sendall right |
|||
And \[Am]this I \[E]bought thee \[Am]gladly |
|||
\end{verse} |
|||
|
|||
\endsong |
Loading…
Reference in new issue